Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
111 lines
4.5 KiB
TypeScript
111 lines
4.5 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { getModel } from "@earendil-works/pi-ai/compat";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { DefaultResourceLoader } from "../src/core/resource-loader.ts";
|
|
import { createAgentSession } from "../src/core/sdk.ts";
|
|
import { SessionManager } from "../src/core/session-manager.ts";
|
|
import { SettingsManager } from "../src/core/settings-manager.ts";
|
|
import { createAllToolDefinitions, createAllTools } from "../src/core/tools/index.ts";
|
|
import { wrapToolDefinition } from "../src/core/tools/tool-definition-wrapper.ts";
|
|
|
|
const strictToolNames = ["read", "bash", "powershell", "edit", "write"] as const;
|
|
|
|
describe("strict built-in tools", () => {
|
|
afterEach(() => vi.unstubAllEnvs());
|
|
|
|
it.each([undefined, "0", "1"])("prefers strict sampling with PI_EXPERIMENTAL=%s", (experimental) => {
|
|
vi.stubEnv("PI_EXPERIMENTAL", experimental);
|
|
const definitions = createAllToolDefinitions(process.cwd());
|
|
const tools = createAllTools(process.cwd());
|
|
for (const name of strictToolNames) {
|
|
expect(definitions[name].constrainedSampling).toEqual({ type: "json_schema", strict: "prefer" });
|
|
expect(tools[name].constrainedSampling).toEqual(definitions[name].constrainedSampling);
|
|
}
|
|
for (const name of ["grep", "find", "ls"] as const) {
|
|
expect(definitions[name].constrainedSampling).toBeUndefined();
|
|
}
|
|
// Strictness is a provider-side conversion, not a change to the execution schema.
|
|
expect(definitions.read.parameters.required).toEqual(["path"]);
|
|
expect(definitions.bash.parameters.required).toEqual(["command"]);
|
|
});
|
|
|
|
it("preserves explicit opt-outs when wrapping definitions for execution", () => {
|
|
const definitions = createAllToolDefinitions(process.cwd());
|
|
for (const name of strictToolNames) {
|
|
const definition = definitions[name];
|
|
const override = { ...definition, constrainedSampling: false as const };
|
|
expect(wrapToolDefinition(override).constrainedSampling).toBe(false);
|
|
expect(override.execute).toBe(definition.execute);
|
|
expect(override.prepareArguments).toBe(definition.prepareArguments);
|
|
expect(override.renderCall).toBe(definition.renderCall);
|
|
expect(override.renderResult).toBe(definition.renderResult);
|
|
expect(override.promptGuidelines).toBe(definition.promptGuidelines);
|
|
expect(definition.constrainedSampling).toEqual({ type: "json_schema", strict: "prefer" });
|
|
}
|
|
});
|
|
|
|
it.each([{ activeTools: [] }, { activeTools: ["read"] }, { activeTools: [...strictToolNames] }])(
|
|
"allows extensions to re-register tools without strict sampling: $activeTools",
|
|
async ({ activeTools }) => {
|
|
const cwd = mkdtempSync(join(tmpdir(), "pi-non-strict-tools-"));
|
|
const agentDir = join(cwd, "agent");
|
|
const settingsManager = SettingsManager.inMemory({ defaultTools: activeTools });
|
|
const resourceLoader = new DefaultResourceLoader({
|
|
cwd,
|
|
agentDir,
|
|
settingsManager,
|
|
noExtensions: true,
|
|
noSkills: true,
|
|
noPromptTemplates: true,
|
|
noThemes: true,
|
|
extensionFactories: [
|
|
(pi) => {
|
|
pi.on("session_start", () => {
|
|
const definitions = createAllToolDefinitions(cwd);
|
|
for (const name of strictToolNames) {
|
|
pi.registerTool({ ...definitions[name], constrainedSampling: false });
|
|
}
|
|
pi.setActiveTools(activeTools);
|
|
});
|
|
},
|
|
],
|
|
});
|
|
try {
|
|
await resourceLoader.reload();
|
|
const { session } = await createAgentSession({
|
|
cwd,
|
|
agentDir,
|
|
model: getModel("anthropic", "claude-sonnet-4-5"),
|
|
settingsManager,
|
|
sessionManager: SessionManager.inMemory(cwd),
|
|
resourceLoader,
|
|
});
|
|
try {
|
|
const originalPrompt = session.systemPrompt;
|
|
await session.bindExtensions({});
|
|
expect(session.getActiveToolNames()).toEqual(activeTools);
|
|
expect(session.systemPrompt).toBe(originalPrompt);
|
|
for (const name of strictToolNames) {
|
|
expect(session.getToolDefinition(name)?.constrainedSampling).toBe(false);
|
|
}
|
|
for (const tool of session.agent.state.tools) {
|
|
expect(tool.constrainedSampling).toBe(false);
|
|
}
|
|
if (activeTools.includes("read")) {
|
|
writeFileSync(join(cwd, "sample.txt"), "still works");
|
|
const read = session.agent.state.tools.find((tool) => tool.name === "read")!;
|
|
const result = await read.execute("read-test", { path: "sample.txt" });
|
|
expect(result.content).toEqual([{ type: "text", text: "still works" }]);
|
|
}
|
|
} finally {
|
|
session.dispose();
|
|
}
|
|
} finally {
|
|
rmSync(cwd, { recursive: true, force: true });
|
|
}
|
|
},
|
|
);
|
|
});
|