feat(agent): return diagnostics from resource loaders
This commit is contained in:
@@ -2,7 +2,11 @@ import { symlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { NodeExecutionEnv } from "../../src/harness/execution-env.js";
|
||||
import { expandPromptTemplate, loadPromptTemplates } from "../../src/harness/prompt-templates.js";
|
||||
import {
|
||||
expandPromptTemplate,
|
||||
loadPromptTemplates,
|
||||
loadSourcedPromptTemplates,
|
||||
} from "../../src/harness/prompt-templates.js";
|
||||
import { createTempDir } from "./session-test-utils.js";
|
||||
|
||||
describe("loadPromptTemplates", () => {
|
||||
@@ -15,21 +19,61 @@ describe("loadPromptTemplates", () => {
|
||||
await env.writeFile("a/nested/ignored.md", "Ignored");
|
||||
await env.writeFile("b/two.md", "First line description\nBody");
|
||||
|
||||
const templates = await loadPromptTemplates(env, ["a", "b"]);
|
||||
const { promptTemplates, diagnostics } = await loadPromptTemplates(env, ["a", "b"]);
|
||||
|
||||
expect(templates).toEqual([
|
||||
expect(diagnostics).toEqual([]);
|
||||
expect(promptTemplates).toEqual([
|
||||
{ name: "one", description: "One template", content: "Hello $1" },
|
||||
{ name: "two", description: "First line description", content: "First line description\nBody" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves source info for sourced prompt templates", async () => {
|
||||
const root = createTempDir();
|
||||
const env = new NodeExecutionEnv({ cwd: root });
|
||||
await env.createDir("prompts", { recursive: true });
|
||||
await env.writeFile("prompts/example.md", "---\ndescription: Example\n---\nExample body");
|
||||
|
||||
const { promptTemplates, diagnostics } = await loadSourcedPromptTemplates(env, [
|
||||
{ path: "prompts", source: { type: "project" as const } },
|
||||
]);
|
||||
|
||||
expect(diagnostics).toEqual([]);
|
||||
expect(promptTemplates).toEqual([
|
||||
{
|
||||
promptTemplate: { name: "example", description: "Example", content: "Example body" },
|
||||
source: { type: "project" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("attaches source info to diagnostics", async () => {
|
||||
const root = createTempDir();
|
||||
const env = new NodeExecutionEnv({ cwd: root });
|
||||
await env.writeFile("broken.md", "---\ndescription: [unterminated\n---\nBody");
|
||||
|
||||
const { promptTemplates, diagnostics } = await loadSourcedPromptTemplates(env, [
|
||||
{ path: "broken.md", source: { type: "user" as const } },
|
||||
]);
|
||||
|
||||
expect(promptTemplates).toEqual([]);
|
||||
expect(diagnostics).toHaveLength(1);
|
||||
expect(diagnostics[0]).toMatchObject({
|
||||
type: "warning",
|
||||
path: join(root, "broken.md"),
|
||||
source: { type: "user" },
|
||||
});
|
||||
});
|
||||
|
||||
it("loads explicit markdown files and symlinked files", async () => {
|
||||
const root = createTempDir();
|
||||
const env = new NodeExecutionEnv({ cwd: root });
|
||||
await env.writeFile("target.md", "---\ndescription: Target\n---\nTarget body");
|
||||
await symlink(join(root, "target.md"), join(root, "link.md"));
|
||||
|
||||
expect(await loadPromptTemplates(env, ["target.md", "link.md"])).toEqual([
|
||||
const { promptTemplates } = await loadPromptTemplates(env, ["target.md", "link.md"]);
|
||||
|
||||
expect(promptTemplates).toEqual([
|
||||
{ name: "target", description: "Target", content: "Target body" },
|
||||
{ name: "link", description: "Target", content: "Target body" },
|
||||
]);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { symlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { NodeExecutionEnv } from "../../src/harness/execution-env.js";
|
||||
import { loadSkills } from "../../src/harness/skills.js";
|
||||
import { loadSkills, loadSourcedSkills } from "../../src/harness/skills.js";
|
||||
import { createTempDir } from "./session-test-utils.js";
|
||||
|
||||
describe("loadSkills", () => {
|
||||
@@ -21,8 +21,9 @@ Use this skill.
|
||||
`,
|
||||
);
|
||||
|
||||
const skills = await loadSkills(env, ".agents/skills");
|
||||
const { skills, diagnostics } = await loadSkills(env, ".agents/skills");
|
||||
|
||||
expect(diagnostics).toEqual([]);
|
||||
expect(skills).toEqual([
|
||||
{
|
||||
name: "example",
|
||||
@@ -44,12 +45,61 @@ Use this skill.
|
||||
);
|
||||
await symlink(join(root, "actual"), join(root, "skills-link"));
|
||||
|
||||
const skills = await loadSkills(env, "skills-link");
|
||||
const { skills } = await loadSkills(env, "skills-link");
|
||||
|
||||
expect(skills.map((skill) => skill.name)).toEqual(["example"]);
|
||||
expect(skills[0]?.filePath).toBe(join(root, "skills-link/example/SKILL.md"));
|
||||
});
|
||||
|
||||
it("preserves source info for sourced skills", async () => {
|
||||
const root = createTempDir();
|
||||
const env = new NodeExecutionEnv({ cwd: root });
|
||||
await env.createDir("user/example", { recursive: true });
|
||||
await env.writeFile(
|
||||
"user/example/SKILL.md",
|
||||
"---\nname: example\ndescription: Example skill\n---\nUse this skill.",
|
||||
);
|
||||
|
||||
const { skills, diagnostics } = await loadSourcedSkills(env, [
|
||||
{ path: "user", source: { type: "user" as const } },
|
||||
]);
|
||||
|
||||
expect(diagnostics).toEqual([]);
|
||||
expect(skills).toEqual([
|
||||
{
|
||||
skill: {
|
||||
name: "example",
|
||||
description: "Example skill",
|
||||
content: "Use this skill.",
|
||||
filePath: join(root, "user/example/SKILL.md"),
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
source: { type: "user" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("attaches source info to diagnostics", async () => {
|
||||
const root = createTempDir();
|
||||
const env = new NodeExecutionEnv({ cwd: root });
|
||||
await env.createDir("user/broken", { recursive: true });
|
||||
await env.writeFile("user/broken/SKILL.md", "---\nname: broken\n---\nMissing description.");
|
||||
|
||||
const { skills, diagnostics } = await loadSourcedSkills(env, [
|
||||
{ path: "user", source: { type: "user" as const } },
|
||||
]);
|
||||
|
||||
expect(skills).toEqual([]);
|
||||
expect(diagnostics).toEqual([
|
||||
{
|
||||
type: "warning",
|
||||
message: "description is required",
|
||||
path: join(root, "user/broken/SKILL.md"),
|
||||
source: { type: "user" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("loads direct markdown children only from the root directory", async () => {
|
||||
const root = createTempDir();
|
||||
const env = new NodeExecutionEnv({ cwd: root });
|
||||
@@ -57,7 +107,7 @@ Use this skill.
|
||||
await env.writeFile("skills/root.md", "---\ndescription: Root skill\n---\nRoot content");
|
||||
await env.writeFile("skills/nested/ignored.md", "---\ndescription: Ignored\n---\nIgnored content");
|
||||
|
||||
const skills = await loadSkills(env, "skills");
|
||||
const { skills } = await loadSkills(env, "skills");
|
||||
|
||||
expect(skills.map((skill) => skill.name)).toEqual(["skills"]);
|
||||
expect(skills[0]?.content).toBe("Root content");
|
||||
|
||||
@@ -1,31 +1,51 @@
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { InMemorySessionStorage } from "../../src/harness/session/storage/memory.js";
|
||||
import {
|
||||
createAgentHarness,
|
||||
formatSkillsForSystemPrompt,
|
||||
loadSkills,
|
||||
loadSourcedPromptTemplates,
|
||||
loadSourcedSkills,
|
||||
NodeExecutionEnv,
|
||||
Session,
|
||||
} from "../../src/index.js";
|
||||
|
||||
type Source = { type: "project" | "user" | "path"; dir: string };
|
||||
|
||||
const env = new NodeExecutionEnv({ cwd: process.cwd() });
|
||||
const skills = await loadSkills(env, "/Users/badlogic/.pi/agent/skills");
|
||||
const source = (type: Source["type"], dir: string) => ({ path: dir, source: { type, dir } });
|
||||
const { skills: sourcedSkills } = await loadSourcedSkills<Source>(env, [
|
||||
source("project", join(env.cwd, ".pi/skills")),
|
||||
source("user", join(homedir(), ".pi/agent/skills")),
|
||||
source("path", join(env.cwd, "../../../pi-skills")),
|
||||
]);
|
||||
const { promptTemplates: sourcedPromptTemplates } = await loadSourcedPromptTemplates<Source>(env, [
|
||||
source("project", join(env.cwd, ".pi/prompts")),
|
||||
source("user", join(homedir(), ".pi/agent/prompts")),
|
||||
]);
|
||||
|
||||
const session = new Session(new InMemorySessionStorage());
|
||||
const agent = createAgentHarness({
|
||||
env,
|
||||
session,
|
||||
model: getModel("openai", "gpt-5.5"),
|
||||
thinkingLevel: "low",
|
||||
systemPrompt: ({ env, resources }) =>
|
||||
[
|
||||
systemPrompt: ({ env, resources }) => {
|
||||
console.log("Building system prompt");
|
||||
return [
|
||||
`You are a helpful assistant.`,
|
||||
formatSkillsForSystemPrompt(resources.skills ?? []),
|
||||
`Current working directory: ${env.cwd}`,
|
||||
]
|
||||
.filter((part) => part.length > 0)
|
||||
.join("\n\n"),
|
||||
resources: { skills },
|
||||
.join("\n\n");
|
||||
},
|
||||
resources: {
|
||||
promptTemplates: sourcedPromptTemplates.map((entry) => entry.promptTemplate),
|
||||
skills: sourcedSkills.map((entry) => entry.skill),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await agent.prompt("What skills do you have?");
|
||||
const response = await agent.prompt("What skills do you have? Any duplicates?");
|
||||
console.log(response);
|
||||
|
||||
Reference in New Issue
Block a user