mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(gateway): quarantine unsupported effective tool schemas
This commit is contained in:
@@ -9,10 +9,13 @@ function mockTool(params: {
|
||||
label: string;
|
||||
description: string;
|
||||
displaySummary?: string;
|
||||
parameters?: unknown;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
...params,
|
||||
parameters: { type: "object", properties: {} },
|
||||
parameters: Object.hasOwn(params, "parameters")
|
||||
? params.parameters
|
||||
: { type: "object", properties: {} },
|
||||
execute: async () => ({ text: params.description }),
|
||||
} as unknown as AnyAgentTool;
|
||||
}
|
||||
@@ -25,6 +28,10 @@ const effectiveInventoryState = vi.hoisted(() => ({
|
||||
pluginMeta: {} as Record<string, { pluginId: string } | undefined>,
|
||||
channelMeta: {} as Record<string, { channelId: string } | undefined>,
|
||||
effectivePolicy: {} as { profile?: string; providerProfile?: string },
|
||||
normalizeToolsMock: vi.fn((options: { tools: AnyAgentTool[] }) => options.tools),
|
||||
staticCatalogModelMock: vi.fn((_options: unknown) => undefined as unknown),
|
||||
dynamicModelMock: vi.fn((_options: unknown) => undefined as unknown),
|
||||
normalizeTransportMock: vi.fn((_options: unknown) => undefined as unknown),
|
||||
createToolsMock: vi.fn<typeof createOpenClawCodingTools>(
|
||||
(_options) =>
|
||||
[
|
||||
@@ -64,6 +71,40 @@ vi.mock("./agent-tools.policy.js", () => ({
|
||||
resolveEffectiveToolPolicy: () => effectiveInventoryState.effectivePolicy,
|
||||
}));
|
||||
|
||||
vi.mock("./runtime-plan/tools.js", () => ({
|
||||
normalizeAgentRuntimeTools: (options: { tools: AnyAgentTool[] }) =>
|
||||
effectiveInventoryState.normalizeToolsMock(options),
|
||||
}));
|
||||
|
||||
vi.mock("./embedded-agent-runner/model.static-catalog.js", () => ({
|
||||
resolveBundledStaticCatalogModel: (options: unknown) =>
|
||||
effectiveInventoryState.staticCatalogModelMock(options),
|
||||
}));
|
||||
|
||||
vi.mock("./embedded-agent-runner/model.js", () => ({
|
||||
resolveModel: (
|
||||
provider: unknown,
|
||||
modelId: unknown,
|
||||
agentDir: unknown,
|
||||
cfg: unknown,
|
||||
options: unknown,
|
||||
) =>
|
||||
({
|
||||
model: effectiveInventoryState.dynamicModelMock({
|
||||
provider,
|
||||
modelId,
|
||||
agentDir,
|
||||
cfg,
|
||||
options,
|
||||
}),
|
||||
}) as unknown,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
normalizeProviderTransportWithPlugin: (options: unknown) =>
|
||||
effectiveInventoryState.normalizeTransportMock(options),
|
||||
}));
|
||||
|
||||
let resolveEffectiveToolInventory: typeof import("./tools-effective-inventory.js").resolveEffectiveToolInventory;
|
||||
|
||||
async function loadHarness(options?: {
|
||||
@@ -72,6 +113,7 @@ async function loadHarness(options?: {
|
||||
pluginMeta?: Record<string, { pluginId: string } | undefined>;
|
||||
channelMeta?: Record<string, { channelId: string } | undefined>;
|
||||
effectivePolicy?: { profile?: string; providerProfile?: string };
|
||||
normalizeToolsMock?: typeof effectiveInventoryState.normalizeToolsMock;
|
||||
}) {
|
||||
effectiveInventoryState.tools = options?.tools ?? [
|
||||
mockTool({ name: "exec", label: "Exec", description: "Run shell commands" }),
|
||||
@@ -80,6 +122,11 @@ async function loadHarness(options?: {
|
||||
effectiveInventoryState.pluginMeta = options?.pluginMeta ?? {};
|
||||
effectiveInventoryState.channelMeta = options?.channelMeta ?? {};
|
||||
effectiveInventoryState.effectivePolicy = options?.effectivePolicy ?? {};
|
||||
effectiveInventoryState.normalizeToolsMock =
|
||||
options?.normalizeToolsMock ?? vi.fn((normalizeOptions) => normalizeOptions.tools);
|
||||
effectiveInventoryState.staticCatalogModelMock = vi.fn((_options: unknown) => undefined);
|
||||
effectiveInventoryState.dynamicModelMock = vi.fn((_options: unknown) => undefined);
|
||||
effectiveInventoryState.normalizeTransportMock = vi.fn((_options: unknown) => undefined);
|
||||
effectiveInventoryState.createToolsMock =
|
||||
options?.createToolsMock ??
|
||||
vi.fn<typeof createOpenClawCodingTools>((_options) => effectiveInventoryState.tools);
|
||||
@@ -102,6 +149,10 @@ describe("resolveEffectiveToolInventory", () => {
|
||||
effectiveInventoryState.pluginMeta = {};
|
||||
effectiveInventoryState.channelMeta = {};
|
||||
effectiveInventoryState.effectivePolicy = {};
|
||||
effectiveInventoryState.normalizeToolsMock = vi.fn((options) => options.tools);
|
||||
effectiveInventoryState.staticCatalogModelMock = vi.fn((_options: unknown) => undefined);
|
||||
effectiveInventoryState.dynamicModelMock = vi.fn((_options: unknown) => undefined);
|
||||
effectiveInventoryState.normalizeTransportMock = vi.fn((_options: unknown) => undefined);
|
||||
effectiveInventoryState.createToolsMock = vi.fn<typeof createOpenClawCodingTools>(
|
||||
(_options) => effectiveInventoryState.tools,
|
||||
);
|
||||
@@ -231,6 +282,423 @@ describe("resolveEffectiveToolInventory", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("quarantines tools with schemas that cannot be projected to the model runtime", async () => {
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({ name: "exec", label: "Exec", description: "Run shell commands" }),
|
||||
mockTool({
|
||||
name: "dofbot_move_angles",
|
||||
label: "Dofbot Move Angles",
|
||||
description: "Move robot joints",
|
||||
parameters: { type: "array", items: { type: "number" } },
|
||||
}),
|
||||
],
|
||||
pluginMeta: { dofbot_move_angles: { pluginId: "dofbot" } },
|
||||
});
|
||||
|
||||
const result = resolveEffectiveToolInventory({ cfg: {} });
|
||||
|
||||
expect(result.groups.flatMap((group) => group.tools.map((tool) => tool.id))).toEqual(["exec"]);
|
||||
expect(result.notices).toEqual([
|
||||
{
|
||||
id: "unsupported-tool-schema:dofbot_move_angles",
|
||||
severity: "warning",
|
||||
message:
|
||||
'Tool "dofbot_move_angles" from plugin "dofbot" has an unsupported runtime input schema (dofbot_move_angles.parameters.type must be "object") and was quarantined before model projection. Fix or disable the owner, or remove the tool from active allowlists.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("validates normalized runtime schemas before quarantining effective tools", async () => {
|
||||
const normalizeToolsMock = vi.fn((options: { tools: AnyAgentTool[] }) =>
|
||||
options.tools.map((entry) =>
|
||||
entry.name === "parameter_free"
|
||||
? ({
|
||||
...entry,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
},
|
||||
} as AnyAgentTool)
|
||||
: entry,
|
||||
),
|
||||
);
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({
|
||||
name: "parameter_free",
|
||||
label: "Parameter Free",
|
||||
description: "Runtime-normalized tool",
|
||||
parameters: undefined,
|
||||
}),
|
||||
],
|
||||
pluginMeta: { parameter_free: { pluginId: "normalized-plugin" } },
|
||||
normalizeToolsMock,
|
||||
});
|
||||
|
||||
const result = resolveEffectiveToolInventory({
|
||||
cfg: {},
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-test",
|
||||
modelApi: "openai-responses",
|
||||
runtimeModel: {
|
||||
id: "gpt-test",
|
||||
name: "GPT Test",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(result.groups[0]?.tools[0]).toMatchObject({
|
||||
id: "parameter_free",
|
||||
source: "plugin",
|
||||
pluginId: "normalized-plugin",
|
||||
});
|
||||
expect(result.notices).toBeUndefined();
|
||||
expect(normalizeToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
modelId: "gpt-test",
|
||||
modelApi: "openai-responses",
|
||||
model: expect.objectContaining({
|
||||
id: "gpt-test",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("overlays provider transport config on bundled static model context", async () => {
|
||||
const normalizeToolsMock = vi.fn((options: { tools: AnyAgentTool[] }) => options.tools);
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({
|
||||
name: "exec",
|
||||
label: "Exec",
|
||||
description: "Run shell commands",
|
||||
}),
|
||||
],
|
||||
normalizeToolsMock,
|
||||
});
|
||||
effectiveInventoryState.staticCatalogModelMock.mockReturnValue({
|
||||
id: "gpt-test",
|
||||
name: "GPT Test",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
});
|
||||
|
||||
resolveEffectiveToolInventory({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-test",
|
||||
});
|
||||
|
||||
expect(normalizeToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelApi: "openai-completions",
|
||||
model: expect.objectContaining({
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(effectiveInventoryState.normalizeTransportMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: "/tmp/workspace-main",
|
||||
context: expect.objectContaining({
|
||||
config: expect.any(Object),
|
||||
workspaceDir: "/tmp/workspace-main",
|
||||
provider: "openai",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(effectiveInventoryState.createToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelApi: "openai-completions",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("applies provider transport normalization to bundled static model context", async () => {
|
||||
const normalizeToolsMock = vi.fn((options: { tools: AnyAgentTool[] }) => options.tools);
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({
|
||||
name: "exec",
|
||||
label: "Exec",
|
||||
description: "Run shell commands",
|
||||
}),
|
||||
],
|
||||
normalizeToolsMock,
|
||||
});
|
||||
effectiveInventoryState.staticCatalogModelMock.mockReturnValue({
|
||||
id: "gpt-test",
|
||||
name: "GPT Test",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
});
|
||||
effectiveInventoryState.normalizeTransportMock.mockReturnValue({
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
});
|
||||
|
||||
resolveEffectiveToolInventory({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-test",
|
||||
});
|
||||
|
||||
expect(normalizeToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelApi: "openai-responses",
|
||||
model: expect.objectContaining({
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(effectiveInventoryState.normalizeTransportMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: "/tmp/workspace-main",
|
||||
context: expect.objectContaining({
|
||||
config: expect.any(Object),
|
||||
workspaceDir: "/tmp/workspace-main",
|
||||
provider: "openai",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(effectiveInventoryState.createToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelApi: "openai-responses",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes configured model context when the model omits api", async () => {
|
||||
const normalizeToolsMock = vi.fn((options: { tools: AnyAgentTool[] }) => options.tools);
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({
|
||||
name: "exec",
|
||||
label: "Exec",
|
||||
description: "Run shell commands",
|
||||
}),
|
||||
],
|
||||
normalizeToolsMock,
|
||||
});
|
||||
effectiveInventoryState.normalizeTransportMock.mockReturnValue({
|
||||
api: "openai-codex-responses",
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
});
|
||||
|
||||
resolveEffectiveToolInventory({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.5-codex",
|
||||
name: "GPT-5.5 Codex",
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 8_192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
modelProvider: "openai-codex",
|
||||
modelId: "gpt-5.5-codex",
|
||||
});
|
||||
|
||||
expect(effectiveInventoryState.normalizeTransportMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: "/tmp/workspace-main",
|
||||
context: expect.objectContaining({
|
||||
config: expect.any(Object),
|
||||
workspaceDir: "/tmp/workspace-main",
|
||||
provider: "openai-codex",
|
||||
api: "openai-responses",
|
||||
baseUrl: undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(effectiveInventoryState.createToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelApi: "openai-codex-responses",
|
||||
}),
|
||||
);
|
||||
expect(normalizeToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelApi: "openai-codex-responses",
|
||||
model: expect.objectContaining({
|
||||
api: "openai-codex-responses",
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves bundled static transport when configured model row omits api", async () => {
|
||||
const normalizeToolsMock = vi.fn((options: { tools: AnyAgentTool[] }) => options.tools);
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({
|
||||
name: "exec",
|
||||
label: "Exec",
|
||||
description: "Run shell commands",
|
||||
}),
|
||||
],
|
||||
normalizeToolsMock,
|
||||
});
|
||||
effectiveInventoryState.staticCatalogModelMock.mockReturnValue({
|
||||
id: "claude-sonnet-test",
|
||||
name: "Bundled Claude Sonnet",
|
||||
provider: "github-copilot",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.githubcopilot.com",
|
||||
});
|
||||
|
||||
resolveEffectiveToolInventory({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
"github-copilot": {
|
||||
models: [
|
||||
{
|
||||
id: "claude-sonnet-test",
|
||||
name: "Configured Claude Sonnet",
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 8_192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
modelProvider: "github-copilot",
|
||||
modelId: "claude-sonnet-test",
|
||||
});
|
||||
|
||||
expect(effectiveInventoryState.createToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelApi: "anthropic-messages",
|
||||
}),
|
||||
);
|
||||
expect(normalizeToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelApi: "anthropic-messages",
|
||||
model: expect.objectContaining({
|
||||
name: "Configured Claude Sonnet",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.githubcopilot.com",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses dynamic provider model context before quarantining runtime-normalized tools", async () => {
|
||||
const normalizeToolsMock = vi.fn((options: { tools: AnyAgentTool[]; modelApi?: string }) =>
|
||||
options.tools.map((entry) =>
|
||||
entry.name === "parameter_free" && options.modelApi === "openai-responses"
|
||||
? ({
|
||||
...entry,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
},
|
||||
} as AnyAgentTool)
|
||||
: entry,
|
||||
),
|
||||
);
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({
|
||||
name: "parameter_free",
|
||||
label: "Parameter Free",
|
||||
description: "Runtime-normalized tool",
|
||||
parameters: undefined,
|
||||
}),
|
||||
],
|
||||
pluginMeta: { parameter_free: { pluginId: "normalized-plugin" } },
|
||||
normalizeToolsMock,
|
||||
});
|
||||
effectiveInventoryState.dynamicModelMock.mockReturnValue({
|
||||
id: "chat-latest",
|
||||
name: "chat-latest",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
});
|
||||
|
||||
const result = resolveEffectiveToolInventory({
|
||||
cfg: {},
|
||||
modelProvider: "openai",
|
||||
modelId: "chat-latest",
|
||||
});
|
||||
|
||||
expect(result.groups[0]?.tools[0]).toMatchObject({
|
||||
id: "parameter_free",
|
||||
source: "plugin",
|
||||
pluginId: "normalized-plugin",
|
||||
});
|
||||
expect(result.notices).toBeUndefined();
|
||||
expect(effectiveInventoryState.dynamicModelMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
modelId: "chat-latest",
|
||||
agentDir: "/tmp/agents/main/agent",
|
||||
options: expect.objectContaining({ workspaceDir: "/tmp/workspace-main" }),
|
||||
}),
|
||||
);
|
||||
expect(normalizeToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
modelId: "chat-latest",
|
||||
modelApi: "openai-responses",
|
||||
model: expect.objectContaining({
|
||||
id: "chat-latest",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not let one plugin project metadata onto another plugin tool", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.toolMetadata = [
|
||||
@@ -408,5 +876,6 @@ describe("resolveEffectiveToolInventory", () => {
|
||||
supportsTools: true,
|
||||
nativeWebSearchTool: true,
|
||||
});
|
||||
expect(createToolsOptions?.modelApi).toBe("openai-completions");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { extractModelCompat } from "../plugins/provider-model-compat.js";
|
||||
import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js";
|
||||
import { normalizeProviderTransportWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { buildPluginToolMetadataKey, getPluginToolMeta } from "../plugins/tools.js";
|
||||
import {
|
||||
@@ -10,11 +12,18 @@ import { resolveAgentDir, resolveAgentWorkspaceDir, resolveSessionAgentId } from
|
||||
import { createOpenClawCodingTools } from "./agent-tools.js";
|
||||
import { resolveEffectiveToolPolicy } from "./agent-tools.policy.js";
|
||||
import { getChannelAgentToolMeta } from "./channel-tools.js";
|
||||
import { resolveModel } from "./embedded-agent-runner/model.js";
|
||||
import { resolveBundledStaticCatalogModel } from "./embedded-agent-runner/model.static-catalog.js";
|
||||
import { normalizeStaticProviderModelId } from "./model-ref-shared.js";
|
||||
import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js";
|
||||
import { normalizeAgentRuntimeTools } from "./runtime-plan/tools.js";
|
||||
import { summarizeToolDescriptionText } from "./tool-description-summary.js";
|
||||
import { resolveToolDisplay } from "./tool-display.js";
|
||||
import { normalizeToolName } from "./tool-policy.js";
|
||||
import {
|
||||
filterRuntimeCompatibleTools,
|
||||
type RuntimeToolSchemaDiagnostic,
|
||||
} from "./tool-schema-projection.js";
|
||||
import type {
|
||||
EffectiveToolInventoryNotice,
|
||||
EffectiveToolInventoryEntry,
|
||||
@@ -47,16 +56,22 @@ function summarizeToolDescription(tool: AnyAgentTool): string {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveEffectiveToolSource(tool: AnyAgentTool): {
|
||||
function resolveEffectiveToolSource(
|
||||
tool: AnyAgentTool,
|
||||
fallbackTool?: AnyAgentTool,
|
||||
): {
|
||||
source: EffectiveToolSource;
|
||||
pluginId?: string;
|
||||
channelId?: string;
|
||||
} {
|
||||
const pluginMeta = getPluginToolMeta(tool);
|
||||
const pluginMeta =
|
||||
getPluginToolMeta(tool) ?? (fallbackTool ? getPluginToolMeta(fallbackTool) : undefined);
|
||||
if (pluginMeta) {
|
||||
return { source: "plugin", pluginId: pluginMeta.pluginId };
|
||||
}
|
||||
const channelMeta = getChannelAgentToolMeta(tool as never);
|
||||
const channelMeta =
|
||||
getChannelAgentToolMeta(tool as never) ??
|
||||
(fallbackTool ? getChannelAgentToolMeta(fallbackTool as never) : undefined);
|
||||
if (channelMeta) {
|
||||
return { source: "channel", channelId: channelMeta.channelId };
|
||||
}
|
||||
@@ -150,6 +165,41 @@ function buildToolInventoryNotices(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildUnsupportedToolSchemaNotice(params: {
|
||||
diagnostic: RuntimeToolSchemaDiagnostic;
|
||||
tool: AnyAgentTool | undefined;
|
||||
fallbackTool: AnyAgentTool | undefined;
|
||||
}): EffectiveToolInventoryNotice {
|
||||
const source = params.tool
|
||||
? resolveEffectiveToolSource(params.tool, params.fallbackTool)
|
||||
: { source: "core" as const };
|
||||
const owner =
|
||||
source.source === "plugin" && source.pluginId
|
||||
? ` from plugin "${source.pluginId}"`
|
||||
: source.source === "channel" && source.channelId
|
||||
? ` from channel "${source.channelId}"`
|
||||
: "";
|
||||
return {
|
||||
id: `unsupported-tool-schema:${params.diagnostic.toolName}`,
|
||||
severity: "warning",
|
||||
message: `Tool "${params.diagnostic.toolName}"${owner} has an unsupported runtime input schema (${params.diagnostic.violations.join(", ")}) and was quarantined before model projection. Fix or disable the owner, or remove the tool from active allowlists.`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildUnsupportedToolSchemaNotices(params: {
|
||||
diagnostics: readonly RuntimeToolSchemaDiagnostic[];
|
||||
tools: readonly AnyAgentTool[];
|
||||
rawToolsByName: ReadonlyMap<string, AnyAgentTool>;
|
||||
}): EffectiveToolInventoryNotice[] {
|
||||
return params.diagnostics.map((diagnostic) =>
|
||||
buildUnsupportedToolSchemaNotice({
|
||||
diagnostic,
|
||||
tool: params.tools[diagnostic.toolIndex],
|
||||
fallbackTool: params.rawToolsByName.get(diagnostic.toolName),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function disambiguateLabels(entries: EffectiveToolInventoryEntry[]): EffectiveToolInventoryEntry[] {
|
||||
const counts = new Map<string, number>();
|
||||
for (const entry of entries) {
|
||||
@@ -164,6 +214,151 @@ function disambiguateLabels(entries: EffectiveToolInventoryEntry[]): EffectiveTo
|
||||
});
|
||||
}
|
||||
|
||||
function applyProviderTransportNormalization(params: {
|
||||
cfg: OpenClawConfig;
|
||||
provider: string;
|
||||
workspaceDir?: string;
|
||||
runtimeModel: ProviderRuntimeModel;
|
||||
}): ProviderRuntimeModel {
|
||||
const normalized = normalizeProviderTransportWithPlugin({
|
||||
provider: params.provider,
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
context: {
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
provider: params.provider,
|
||||
api: params.runtimeModel.api,
|
||||
baseUrl: params.runtimeModel.baseUrl,
|
||||
},
|
||||
});
|
||||
if (!normalized) {
|
||||
return params.runtimeModel;
|
||||
}
|
||||
return {
|
||||
...params.runtimeModel,
|
||||
api: normalized.api ?? params.runtimeModel.api,
|
||||
baseUrl: normalized.baseUrl ?? params.runtimeModel.baseUrl,
|
||||
} as ProviderRuntimeModel;
|
||||
}
|
||||
|
||||
function resolveConfiguredFallbackApi(
|
||||
providerConfig: { api?: string; baseUrl?: string } | undefined,
|
||||
): string {
|
||||
const explicitApi = normalizeOptionalString(providerConfig?.api);
|
||||
if (explicitApi) {
|
||||
return explicitApi;
|
||||
}
|
||||
return normalizeOptionalString(providerConfig?.baseUrl)
|
||||
? "openai-completions"
|
||||
: "openai-responses";
|
||||
}
|
||||
|
||||
function resolveDynamicRuntimeModelContext(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
}): { modelApi?: string; runtimeModel?: ProviderRuntimeModel } {
|
||||
const runtimeModel = resolveModel(params.provider, params.modelId, params.agentDir, params.cfg, {
|
||||
workspaceDir: params.workspaceDir,
|
||||
}).model as ProviderRuntimeModel | undefined;
|
||||
if (!runtimeModel) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
modelApi: runtimeModel.api,
|
||||
runtimeModel,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveEffectiveToolInventoryRuntimeModelContext(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
modelProvider?: string;
|
||||
modelId?: string;
|
||||
}): { modelApi?: string; runtimeModel?: ProviderRuntimeModel } {
|
||||
const provider = normalizeProviderId(params.modelProvider ?? "");
|
||||
const modelId = params.modelId?.trim() ?? "";
|
||||
if (!provider || !modelId) {
|
||||
return {};
|
||||
}
|
||||
const agentId = params.agentId?.trim() || resolveSessionAgentId({ config: params.cfg });
|
||||
const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId);
|
||||
const providerConfig = findNormalizedProviderValue(params.cfg.models?.providers, provider);
|
||||
const configuredModels = Array.isArray(providerConfig?.models) ? providerConfig.models : [];
|
||||
const normalizedModelId = normalizeStaticProviderModelId(provider, modelId);
|
||||
const normalizedModelKey = normalizeLowercaseStringOrEmpty(normalizedModelId);
|
||||
const providerPrefixedModelKey = normalizeLowercaseStringOrEmpty(
|
||||
`${provider}/${normalizedModelId}`,
|
||||
);
|
||||
const configuredModel = configuredModels.find((model) => {
|
||||
const id = normalizeStaticProviderModelId(provider, model.id);
|
||||
const key = normalizeLowercaseStringOrEmpty(id);
|
||||
return key === normalizedModelKey || key === providerPrefixedModelKey;
|
||||
});
|
||||
const bundledStaticModel = resolveBundledStaticCatalogModel({
|
||||
provider,
|
||||
modelId,
|
||||
cfg: params.cfg,
|
||||
workspaceDir,
|
||||
}) as ProviderRuntimeModel | undefined;
|
||||
if (configuredModel) {
|
||||
const configuredApi =
|
||||
normalizeOptionalString(configuredModel.api) ??
|
||||
normalizeOptionalString(providerConfig?.api) ??
|
||||
normalizeOptionalString(bundledStaticModel?.api) ??
|
||||
resolveConfiguredFallbackApi(providerConfig);
|
||||
const runtimeModel = applyProviderTransportNormalization({
|
||||
cfg: params.cfg,
|
||||
provider,
|
||||
workspaceDir,
|
||||
runtimeModel: {
|
||||
...bundledStaticModel,
|
||||
...configuredModel,
|
||||
id: configuredModel.id,
|
||||
name: configuredModel.name ?? bundledStaticModel?.name ?? configuredModel.id,
|
||||
provider,
|
||||
api: configuredApi,
|
||||
baseUrl:
|
||||
normalizeOptionalString(configuredModel.baseUrl) ??
|
||||
normalizeOptionalString(providerConfig?.baseUrl) ??
|
||||
normalizeOptionalString(bundledStaticModel?.baseUrl),
|
||||
} as ProviderRuntimeModel,
|
||||
});
|
||||
return {
|
||||
modelApi: runtimeModel.api,
|
||||
runtimeModel,
|
||||
};
|
||||
}
|
||||
if (!bundledStaticModel) {
|
||||
return resolveDynamicRuntimeModelContext({
|
||||
cfg: params.cfg,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir,
|
||||
provider,
|
||||
modelId,
|
||||
});
|
||||
}
|
||||
const runtimeModel = applyProviderTransportNormalization({
|
||||
cfg: params.cfg,
|
||||
provider,
|
||||
workspaceDir,
|
||||
runtimeModel: {
|
||||
...bundledStaticModel,
|
||||
api: normalizeOptionalString(providerConfig?.api) ?? bundledStaticModel.api,
|
||||
baseUrl: normalizeOptionalString(providerConfig?.baseUrl) ?? bundledStaticModel.baseUrl,
|
||||
} as ProviderRuntimeModel,
|
||||
});
|
||||
return {
|
||||
modelApi: runtimeModel.api,
|
||||
runtimeModel,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveEffectiveModelCompat(params: {
|
||||
cfg: OpenClawConfig;
|
||||
modelProvider?: string;
|
||||
@@ -200,6 +395,20 @@ export function resolveEffectiveToolInventory(
|
||||
resolveSessionAgentId({ sessionKey: params.sessionKey, config: params.cfg });
|
||||
const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId);
|
||||
const agentDir = params.agentDir ?? resolveAgentDir(params.cfg, agentId);
|
||||
const runtimeModelContext =
|
||||
params.modelApi || params.runtimeModel
|
||||
? {
|
||||
modelApi: params.modelApi ?? params.runtimeModel?.api,
|
||||
runtimeModel: params.runtimeModel,
|
||||
}
|
||||
: resolveEffectiveToolInventoryRuntimeModelContext({
|
||||
cfg: params.cfg,
|
||||
agentId,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
modelProvider: params.modelProvider,
|
||||
modelId: params.modelId,
|
||||
});
|
||||
const modelCompat = resolveEffectiveModelCompat({
|
||||
cfg: params.cfg,
|
||||
modelProvider: params.modelProvider,
|
||||
@@ -214,6 +423,7 @@ export function resolveEffectiveToolInventory(
|
||||
config: params.cfg,
|
||||
modelProvider: params.modelProvider,
|
||||
modelId: params.modelId,
|
||||
modelApi: runtimeModelContext.modelApi,
|
||||
modelCompat,
|
||||
messageProvider: params.messageProvider,
|
||||
senderId: params.senderId,
|
||||
@@ -233,6 +443,17 @@ export function resolveEffectiveToolInventory(
|
||||
requireExplicitMessageTarget: params.requireExplicitMessageTarget,
|
||||
disableMessageTool: params.disableMessageTool,
|
||||
});
|
||||
const rawToolsByName = new Map(effectiveTools.map((tool) => [tool.name, tool]));
|
||||
const normalizedEffectiveTools = normalizeAgentRuntimeTools({
|
||||
tools: effectiveTools,
|
||||
provider: params.modelProvider ?? "",
|
||||
config: params.cfg,
|
||||
workspaceDir,
|
||||
modelId: params.modelId,
|
||||
modelApi: runtimeModelContext.modelApi,
|
||||
model: runtimeModelContext.runtimeModel,
|
||||
});
|
||||
const toolSchemaProjection = filterRuntimeCompatibleTools(normalizedEffectiveTools);
|
||||
const effectivePolicy = resolveEffectiveToolPolicy({
|
||||
config: params.cfg,
|
||||
agentId,
|
||||
@@ -251,9 +472,9 @@ export function resolveEffectiveToolInventory(
|
||||
);
|
||||
|
||||
const entries = disambiguateLabels(
|
||||
effectiveTools
|
||||
toolSchemaProjection.tools
|
||||
.map((tool) => {
|
||||
const source = resolveEffectiveToolSource(tool);
|
||||
const source = resolveEffectiveToolSource(tool, rawToolsByName.get(tool.name));
|
||||
const metadata = source.pluginId
|
||||
? pluginToolMetadata.get(buildPluginToolMetadataKey(source.pluginId, tool.name))
|
||||
: undefined;
|
||||
@@ -276,7 +497,14 @@ export function resolveEffectiveToolInventory(
|
||||
})
|
||||
.toSorted((a, b) => a.label.localeCompare(b.label)),
|
||||
);
|
||||
const notices = buildToolInventoryNotices({ cfg: params.cfg, profile, entries, effectivePolicy });
|
||||
const notices = [
|
||||
...buildUnsupportedToolSchemaNotices({
|
||||
diagnostics: toolSchemaProjection.diagnostics,
|
||||
tools: normalizedEffectiveTools,
|
||||
rawToolsByName,
|
||||
}),
|
||||
...(buildToolInventoryNotices({ cfg: params.cfg, profile, entries, effectivePolicy }) ?? []),
|
||||
];
|
||||
const groupsBySource = new Map<EffectiveToolSource, EffectiveToolInventoryEntry[]>();
|
||||
for (const entry of entries) {
|
||||
const tools = groupsBySource.get(entry.source) ?? [];
|
||||
@@ -299,5 +527,5 @@ export function resolveEffectiveToolInventory(
|
||||
})
|
||||
.filter((group): group is EffectiveToolInventoryGroup => group !== null);
|
||||
|
||||
return { agentId, profile, groups, ...(notices ? { notices } : {}) };
|
||||
return { agentId, profile, groups, ...(notices.length > 0 ? { notices } : {}) };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js";
|
||||
|
||||
export type EffectiveToolSource = "core" | "plugin" | "channel";
|
||||
|
||||
@@ -48,6 +49,8 @@ export type ResolveEffectiveToolInventoryParams = {
|
||||
accountId?: string | null;
|
||||
modelProvider?: string;
|
||||
modelId?: string;
|
||||
modelApi?: string | null;
|
||||
runtimeModel?: ProviderRuntimeModel;
|
||||
currentChannelId?: string;
|
||||
currentThreadTs?: string;
|
||||
currentMessageId?: string | number;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Value } from "typebox/value";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ToolsEffectiveResultSchema } from "./agents-models-skills.js";
|
||||
|
||||
function toolsEffectiveResult() {
|
||||
return {
|
||||
agentId: "main",
|
||||
profile: "full",
|
||||
groups: [
|
||||
{
|
||||
id: "core",
|
||||
label: "Built-in tools",
|
||||
source: "core",
|
||||
tools: [
|
||||
{
|
||||
id: "exec",
|
||||
label: "Exec",
|
||||
description: "Run shell commands",
|
||||
rawDescription: "Run shell commands",
|
||||
source: "core",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("ToolsEffectiveResultSchema", () => {
|
||||
it("accepts runtime tool quarantine notices", () => {
|
||||
const result = {
|
||||
...toolsEffectiveResult(),
|
||||
notices: [
|
||||
{
|
||||
id: "unsupported-tool-schema:dofbot_move_angles",
|
||||
severity: "warning",
|
||||
message:
|
||||
'Tool "dofbot_move_angles" from plugin "dofbot" has an unsupported runtime input schema and was quarantined before model projection.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(Value.Check(ToolsEffectiveResultSchema, result)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps tool quarantine notices strict", () => {
|
||||
const result = {
|
||||
...toolsEffectiveResult(),
|
||||
notices: [
|
||||
{
|
||||
id: "unsupported-tool-schema:dofbot_move_angles",
|
||||
severity: "warning",
|
||||
message: "Unsupported schema.",
|
||||
extra: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(Value.Check(ToolsEffectiveResultSchema, result)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -593,11 +593,21 @@ export const ToolsEffectiveGroupSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ToolsEffectiveNoticeSchema = Type.Object(
|
||||
{
|
||||
id: NonEmptyString,
|
||||
severity: Type.Union([Type.Literal("info"), Type.Literal("warning")]),
|
||||
message: Type.String(),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ToolsEffectiveResultSchema = Type.Object(
|
||||
{
|
||||
agentId: NonEmptyString,
|
||||
profile: NonEmptyString,
|
||||
groups: Type.Array(ToolsEffectiveGroupSchema),
|
||||
notices: Type.Optional(Type.Array(ToolsEffectiveNoticeSchema)),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
ToolsCatalogResultSchema,
|
||||
ToolsEffectiveEntrySchema,
|
||||
ToolsEffectiveGroupSchema,
|
||||
ToolsEffectiveNoticeSchema,
|
||||
ToolsEffectiveParamsSchema,
|
||||
ToolsEffectiveResultSchema,
|
||||
ToolsInvokeErrorSchema,
|
||||
@@ -453,6 +454,7 @@ export const ProtocolSchemas = {
|
||||
ToolsEffectiveParams: ToolsEffectiveParamsSchema,
|
||||
ToolsEffectiveEntry: ToolsEffectiveEntrySchema,
|
||||
ToolsEffectiveGroup: ToolsEffectiveGroupSchema,
|
||||
ToolsEffectiveNotice: ToolsEffectiveNoticeSchema,
|
||||
ToolsEffectiveResult: ToolsEffectiveResultSchema,
|
||||
ToolsInvokeParams: ToolsInvokeParamsSchema,
|
||||
ToolsInvokeError: ToolsInvokeErrorSchema,
|
||||
|
||||
@@ -177,6 +177,7 @@ export type ToolsCatalogResult = SchemaType<"ToolsCatalogResult">;
|
||||
export type ToolsEffectiveParams = SchemaType<"ToolsEffectiveParams">;
|
||||
export type ToolsEffectiveEntry = SchemaType<"ToolsEffectiveEntry">;
|
||||
export type ToolsEffectiveGroup = SchemaType<"ToolsEffectiveGroup">;
|
||||
export type ToolsEffectiveNotice = SchemaType<"ToolsEffectiveNotice">;
|
||||
export type ToolsEffectiveResult = SchemaType<"ToolsEffectiveResult">;
|
||||
export type ToolsInvokeParams = SchemaType<"ToolsInvokeParams">;
|
||||
export type ToolsInvokeResult = SchemaType<"ToolsInvokeResult">;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export { listAgentIds, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
export { resolveEffectiveToolInventory } from "../../agents/tools-effective-inventory.js";
|
||||
export { listAgentIds, resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
export {
|
||||
resolveEffectiveToolInventory,
|
||||
resolveEffectiveToolInventoryRuntimeModelContext,
|
||||
} from "../../agents/tools-effective-inventory.js";
|
||||
export { resolveReplyToMode } from "../../auto-reply/reply/reply-threading.js";
|
||||
export { resolveRuntimeConfigCacheKey } from "../../config/config.js";
|
||||
export {
|
||||
|
||||
@@ -32,6 +32,7 @@ const runtimeMocks = vi.hoisted(() => ({
|
||||
getActivePluginChannelRegistryVersion: vi.fn(() => 1),
|
||||
getActivePluginRegistryVersion: vi.fn(() => 1),
|
||||
resolveRuntimeConfigCacheKey: vi.fn(() => "runtime:1:test"),
|
||||
resolveAgentDir: vi.fn(() => "/tmp/agents/main/agent"),
|
||||
resolveEffectiveToolInventory: vi.fn(() => ({
|
||||
agentId: "main",
|
||||
profile: "coding",
|
||||
@@ -55,6 +56,16 @@ const runtimeMocks = vi.hoisted(() => ({
|
||||
resolveReplyToMode: vi.fn(() => "first"),
|
||||
resolveSessionAgentId: vi.fn(() => "main"),
|
||||
resolveSessionModelRef: vi.fn(() => ({ provider: "openai", model: "gpt-4.1" })),
|
||||
resolveEffectiveToolInventoryRuntimeModelContext: vi.fn(() => ({
|
||||
modelApi: "openai-responses",
|
||||
runtimeModel: {
|
||||
id: "gpt-4.1",
|
||||
name: "GPT 4.1",
|
||||
provider: "openai",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./tools-effective.runtime.js", () => runtimeMocks);
|
||||
@@ -184,6 +195,22 @@ describe("tools.effective handler", () => {
|
||||
expect(inventoryParams?.messageProvider).toBe("telegram");
|
||||
expect(inventoryParams?.modelProvider).toBe("openai");
|
||||
expect(inventoryParams?.modelId).toBe("gpt-4.1");
|
||||
expect(inventoryParams?.agentDir).toBe("/tmp/agents/main/agent");
|
||||
expect(inventoryParams?.modelApi).toBe("openai-responses");
|
||||
expect(inventoryParams?.runtimeModel).toMatchObject({
|
||||
id: "gpt-4.1",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
});
|
||||
expect(runtimeMocks.resolveEffectiveToolInventoryRuntimeModelContext).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeMocks.resolveEffectiveToolInventoryRuntimeModelContext).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "main",
|
||||
agentDir: "/tmp/agents/main/agent",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4.1",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("serves repeated requests from the fresh inventory cache", async () => {
|
||||
@@ -193,6 +220,7 @@ describe("tools.effective handler", () => {
|
||||
await second.invoke();
|
||||
|
||||
expect(runtimeMocks.resolveEffectiveToolInventory).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeMocks.resolveEffectiveToolInventoryRuntimeModelContext).toHaveBeenCalledTimes(1);
|
||||
expect(firstRespondCall(first.respond)?.[0]).toBe(true);
|
||||
expect(firstRespondCall(second.respond)?.[0]).toBe(true);
|
||||
});
|
||||
@@ -209,6 +237,27 @@ describe("tools.effective handler", () => {
|
||||
expect(firstRespondCall(second.respond)?.[0]).toBe(true);
|
||||
});
|
||||
|
||||
it("does not resolve runtime model context for fresh inventory cache hits", async () => {
|
||||
const first = createInvokeParams({ sessionKey: "main:abc" });
|
||||
await first.invoke();
|
||||
|
||||
runtimeMocks.resolveEffectiveToolInventoryRuntimeModelContext.mockReturnValueOnce({
|
||||
modelApi: "openai-completions",
|
||||
runtimeModel: {
|
||||
id: "gpt-4.1",
|
||||
name: "GPT 4.1",
|
||||
provider: "openai",
|
||||
api: "openai-completions",
|
||||
},
|
||||
} as never);
|
||||
const second = createInvokeParams({ sessionKey: "main:abc" });
|
||||
await second.invoke();
|
||||
|
||||
expect(runtimeMocks.resolveEffectiveToolInventory).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeMocks.resolveEffectiveToolInventoryRuntimeModelContext).toHaveBeenCalledTimes(1);
|
||||
expect(firstRespondCall(second.respond)?.[0]).toBe(true);
|
||||
});
|
||||
|
||||
it("coalesces identical cache misses while inventory resolution is pending", async () => {
|
||||
const first = createInvokeParams({ sessionKey: "main:abc" });
|
||||
const second = createInvokeParams({ sessionKey: "main:abc" });
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
getActivePluginRegistryVersion,
|
||||
listAgentIds,
|
||||
loadSessionEntry,
|
||||
resolveAgentDir,
|
||||
resolveEffectiveToolInventory,
|
||||
resolveEffectiveToolInventoryRuntimeModelContext,
|
||||
resolveReplyToMode,
|
||||
resolveRuntimeConfigCacheKey,
|
||||
resolveSessionAgentId,
|
||||
@@ -132,13 +134,24 @@ function scheduleToolsEffectiveRefresh(
|
||||
const task = new Promise<EffectiveToolInventoryResult>((resolve, reject) => {
|
||||
setImmediate(() => {
|
||||
try {
|
||||
const agentDir = resolveAgentDir(context.cfg, context.agentId);
|
||||
const runtimeModelContext = resolveEffectiveToolInventoryRuntimeModelContext({
|
||||
cfg: context.cfg,
|
||||
agentId: context.agentId,
|
||||
agentDir,
|
||||
modelProvider: context.modelProvider,
|
||||
modelId: context.modelId,
|
||||
});
|
||||
const value = resolveEffectiveToolInventory({
|
||||
cfg: context.cfg,
|
||||
agentId: context.agentId,
|
||||
agentDir,
|
||||
sessionKey: context.sessionKey,
|
||||
messageProvider: context.messageProvider,
|
||||
modelProvider: context.modelProvider,
|
||||
modelId: context.modelId,
|
||||
modelApi: runtimeModelContext.modelApi,
|
||||
runtimeModel: runtimeModelContext.runtimeModel,
|
||||
currentChannelId: context.currentChannelId,
|
||||
currentThreadTs: context.currentThreadTs,
|
||||
accountId: context.accountId,
|
||||
|
||||
Reference in New Issue
Block a user