mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
8a0903ebf2
The root barrel is now core-only and side-effect free: types, createModels/createProvider, auth substrate, lazyStream/lazyApi, faux, utils. Generated catalogs, api-registry, env-api-keys, images, global stream functions, and per-API lazy wrappers leave the root. New @earendil-works/pi-ai/compat preserves the old surface verbatim as a strict superset of the root: api-dispatch stream/complete with env key injection, the builtin registration side effect (skip-if-present so it cannot clobber earlier overrides), deprecated getModel/getModels/ getProviders aliases of the new getBuiltin* reads in providers/all, lazy api wrappers + setBedrockProviderModule, and image generation. Compat dies with the coding-agent ModelManager migration. Packaging: exports map gains ./compat, ./providers/*, ./api/*; sideEffects array lists only the effectful modules. Old-global imports across agent/coding-agent/examples and pi-ai tests switch to /compat (path-only; compat is a superset). The coding-agent extension loader resolves the pi-ai ROOT specifier to compat, so existing user extensions using the old global API keep working at runtime until compat is removed. vitest configs alias /compat to src; browser smoke imports old globals from /compat.
186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { getModel } from "@earendil-works/pi-ai/compat";
|
|
import { Type } from "typebox";
|
|
import { afterEach, beforeEach, describe, expect, it } 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";
|
|
|
|
describe("AgentSession dynamic tool registration", () => {
|
|
let tempDir: string;
|
|
let agentDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = join(tmpdir(), `pi-dynamic-tool-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
agentDir = join(tempDir, "agent");
|
|
mkdirSync(agentDir, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (tempDir && existsSync(tempDir)) {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("refreshes tool registry when tools are registered after initialization", async () => {
|
|
const settingsManager = SettingsManager.create(tempDir, agentDir);
|
|
const sessionManager = SessionManager.inMemory();
|
|
|
|
const resourceLoader = new DefaultResourceLoader({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
settingsManager,
|
|
extensionFactories: [
|
|
(pi) => {
|
|
pi.on("session_start", () => {
|
|
pi.registerTool({
|
|
name: "dynamic_tool",
|
|
label: "Dynamic Tool",
|
|
description: "Tool registered from session_start",
|
|
promptSnippet: "Run dynamic test behavior",
|
|
promptGuidelines: ["Use dynamic_tool when the user asks for dynamic behavior tests."],
|
|
parameters: Type.Object({}),
|
|
execute: async () => ({
|
|
content: [{ type: "text", text: "ok" }],
|
|
details: {},
|
|
}),
|
|
});
|
|
});
|
|
},
|
|
],
|
|
});
|
|
await resourceLoader.reload();
|
|
|
|
const { session } = await createAgentSession({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
model: getModel("anthropic", "claude-sonnet-4-5")!,
|
|
settingsManager,
|
|
sessionManager,
|
|
resourceLoader,
|
|
});
|
|
|
|
expect(session.getAllTools().map((tool) => tool.name)).not.toContain("dynamic_tool");
|
|
|
|
await session.bindExtensions({});
|
|
|
|
const allTools = session.getAllTools();
|
|
const dynamicTool = allTools.find((tool) => tool.name === "dynamic_tool");
|
|
const readTool = allTools.find((tool) => tool.name === "read");
|
|
|
|
expect(allTools.map((tool) => tool.name)).toContain("dynamic_tool");
|
|
expect(dynamicTool?.promptGuidelines).toEqual([
|
|
"Use dynamic_tool when the user asks for dynamic behavior tests.",
|
|
]);
|
|
expect(dynamicTool?.sourceInfo).toMatchObject({
|
|
path: "<inline:1>",
|
|
source: "inline",
|
|
scope: "temporary",
|
|
origin: "top-level",
|
|
});
|
|
expect(readTool?.sourceInfo).toMatchObject({
|
|
path: "<builtin:read>",
|
|
source: "builtin",
|
|
scope: "temporary",
|
|
origin: "top-level",
|
|
});
|
|
expect(session.getActiveToolNames()).toContain("dynamic_tool");
|
|
expect(session.systemPrompt).toContain("- dynamic_tool: Run dynamic test behavior");
|
|
expect(session.systemPrompt).toContain("- Use dynamic_tool when the user asks for dynamic behavior tests.");
|
|
|
|
session.dispose();
|
|
});
|
|
|
|
it("returns source metadata for SDK custom tools", async () => {
|
|
const settingsManager = SettingsManager.create(tempDir, agentDir);
|
|
const sessionManager = SessionManager.inMemory();
|
|
const resourceLoader = new DefaultResourceLoader({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
settingsManager,
|
|
});
|
|
await resourceLoader.reload();
|
|
|
|
const { session } = await createAgentSession({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
model: getModel("anthropic", "claude-sonnet-4-5")!,
|
|
settingsManager,
|
|
sessionManager,
|
|
resourceLoader,
|
|
customTools: [
|
|
{
|
|
name: "sdk_tool",
|
|
label: "SDK Tool",
|
|
description: "Tool registered through createAgentSession",
|
|
parameters: Type.Object({}),
|
|
execute: async () => ({
|
|
content: [{ type: "text", text: "ok" }],
|
|
details: {},
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
const sdkTool = session.getAllTools().find((tool) => tool.name === "sdk_tool");
|
|
expect(sdkTool?.sourceInfo).toMatchObject({
|
|
path: "<sdk:sdk_tool>",
|
|
source: "sdk",
|
|
scope: "temporary",
|
|
origin: "top-level",
|
|
});
|
|
expect(session.getActiveToolNames()).toContain("sdk_tool");
|
|
|
|
session.dispose();
|
|
});
|
|
|
|
it("keeps custom tools active but omits them from available tools when promptSnippet is not provided", async () => {
|
|
const settingsManager = SettingsManager.create(tempDir, agentDir);
|
|
const sessionManager = SessionManager.inMemory();
|
|
|
|
const resourceLoader = new DefaultResourceLoader({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
settingsManager,
|
|
extensionFactories: [
|
|
(pi) => {
|
|
pi.on("session_start", () => {
|
|
pi.registerTool({
|
|
name: "hidden_tool",
|
|
label: "Hidden Tool",
|
|
description: "Description should not appear in available tools",
|
|
parameters: Type.Object({}),
|
|
execute: async () => ({
|
|
content: [{ type: "text", text: "ok" }],
|
|
details: {},
|
|
}),
|
|
});
|
|
});
|
|
},
|
|
],
|
|
});
|
|
await resourceLoader.reload();
|
|
|
|
const { session } = await createAgentSession({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
model: getModel("anthropic", "claude-sonnet-4-5")!,
|
|
settingsManager,
|
|
sessionManager,
|
|
resourceLoader,
|
|
});
|
|
|
|
await session.bindExtensions({});
|
|
|
|
expect(session.getAllTools().map((tool) => tool.name)).toContain("hidden_tool");
|
|
expect(session.getActiveToolNames()).toContain("hidden_tool");
|
|
expect(session.systemPrompt).not.toContain("hidden_tool");
|
|
expect(session.systemPrompt).not.toContain("Description should not appear in available tools");
|
|
|
|
session.dispose();
|
|
});
|
|
});
|