fix(core): present the project dir to the model as the App Data Dir in the default system prompt (#61)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14b5e6384f
commit
816a823b75
24 files changed
+423
-68
No files matched your search
@@ -26,6 +26,8 @@ import {
|
||||
buildToolConfig,
|
||||
selectBuiltinToolsForModel,
|
||||
defaultProjectConfig,
|
||||
defaultSystemConfig,
|
||||
resetSystemConfigToDefaults,
|
||||
getModel,
|
||||
isValidVaultKey,
|
||||
loadOrInitAgentState,
|
||||
@@ -313,6 +315,12 @@ describe("assembleSystemPrompt", () => {
|
||||
expect(prompt).not.toContain(PLATFORM_PLACEHOLDER);
|
||||
expect(prompt).not.toContain(OS_VERSION_PLACEHOLDER);
|
||||
expect(prompt).not.toContain(DATE_PLACEHOLDER);
|
||||
expect(prompt).not.toContain(PROJECT_DIR_PLACEHOLDER);
|
||||
// The project dir is labeled "App Data Dir" (the app data root, not the task's
|
||||
// directory) — the raw "Project Dir" label must never reach the model.
|
||||
expect(prompt).toContain("App Data Dir: /tmp/proj");
|
||||
expect(prompt).not.toContain("Project Dir:");
|
||||
expect(prompt).not.toContain("<project_dir>");
|
||||
});
|
||||
|
||||
it("default prompt carries the port and API-key guardrails", async () => {
|
||||
@@ -498,7 +506,7 @@ describe("assembleSystemPrompt", () => {
|
||||
expect(prompt).toContain("Session ID: session-test-1");
|
||||
expect(prompt).toContain("CWD: /tmp/penguin-ws");
|
||||
expect(prompt).toContain("Agent ID: agent-x");
|
||||
expect(prompt).toContain("Project Dir: /tmp/proj");
|
||||
expect(prompt).toContain("App Data Dir: /tmp/proj");
|
||||
expect(prompt).toContain("Provider: openai");
|
||||
expect(prompt).toContain("Model ID: gpt-5.5");
|
||||
expect(prompt).toContain("Platform:");
|
||||
@@ -506,8 +514,8 @@ describe("assembleSystemPrompt", () => {
|
||||
expect(prompt).toContain("Date: 2026-06-30");
|
||||
expect(prompt.indexOf("Platform:")).toBeLessThan(prompt.indexOf("OS Version:"));
|
||||
expect(prompt.indexOf("OS Version:")).toBeLessThan(prompt.indexOf("Date:"));
|
||||
expect(prompt.indexOf("Date:")).toBeLessThan(prompt.indexOf("Project Dir:"));
|
||||
expect(prompt.indexOf("Project Dir:")).toBeLessThan(prompt.indexOf("Agent ID:"));
|
||||
expect(prompt.indexOf("Date:")).toBeLessThan(prompt.indexOf("App Data Dir:"));
|
||||
expect(prompt.indexOf("App Data Dir:")).toBeLessThan(prompt.indexOf("Agent ID:"));
|
||||
expect(prompt.indexOf("Agent ID:")).toBeLessThan(prompt.indexOf("CWD:"));
|
||||
expect(prompt.indexOf("CWD:")).toBeLessThan(prompt.indexOf("Provider:"));
|
||||
expect(prompt.indexOf("Provider:")).toBeLessThan(prompt.indexOf("Model ID:"));
|
||||
@@ -515,6 +523,81 @@ describe("assembleSystemPrompt", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetSystemConfigToDefaults", () => {
|
||||
it("replaces everything with the current defaults, keeping only name/description/version", async () => {
|
||||
await loadOrInitAgentState();
|
||||
const configPath = systemConfigPath(tmpRoot, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID);
|
||||
// An old on-disk config: custom prompt/runtime/tools plus a key outside the schema.
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
[
|
||||
"name: Custom Name",
|
||||
"description: Custom description",
|
||||
"version: 7",
|
||||
"system_prompt: Custom prompt with {{PROJECT_DIR}}",
|
||||
"max_turns: 5",
|
||||
"model:",
|
||||
" max_tokens: 1234",
|
||||
"compaction:",
|
||||
" mode: discard",
|
||||
"tools:",
|
||||
" builtin: []",
|
||||
" mcpServers:",
|
||||
" - name: custom-mcp",
|
||||
" config: {}",
|
||||
"custom_extra_key: should-be-dropped",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const written = await resetSystemConfigToDefaults(
|
||||
tmpRoot,
|
||||
DEFAULT_PROJECT_ID,
|
||||
DEFAULT_AGENT_ID,
|
||||
);
|
||||
// Identity fields survive; everything else is the current default.
|
||||
expect(written.name).toBe("Custom Name");
|
||||
expect(written.description).toBe("Custom description");
|
||||
expect(written.version).toBe(7);
|
||||
const defaults = defaultSystemConfig();
|
||||
expect(written.system_prompt).toBe(defaults.system_prompt);
|
||||
expect(written.max_turns).toBe(defaults.max_turns);
|
||||
expect(written.model).toEqual(defaults.model);
|
||||
expect(written.compaction).toEqual(defaults.compaction);
|
||||
expect(written.tools).toEqual(defaults.tools);
|
||||
|
||||
// The file on disk round-trips to the same object; out-of-schema keys are gone.
|
||||
const reloaded = await loadOrInitAgentState();
|
||||
expect(reloaded.systemConfig).toEqual(written);
|
||||
expect("custom_extra_key" in reloaded.systemConfig).toBe(false);
|
||||
expect(reloaded.systemConfig.system_prompt).toContain("App Data Dir: {{PROJECT_DIR}}");
|
||||
});
|
||||
|
||||
it("normalizes an invalid version to 1 and keeps a missing name/description absent", async () => {
|
||||
await loadOrInitAgentState();
|
||||
const configPath = systemConfigPath(tmpRoot, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID);
|
||||
await fs.writeFile(configPath, "system_prompt: old\nversion: nonsense\n", "utf8");
|
||||
const written = await resetSystemConfigToDefaults(
|
||||
tmpRoot,
|
||||
DEFAULT_PROJECT_ID,
|
||||
DEFAULT_AGENT_ID,
|
||||
);
|
||||
expect(written.version).toBe(1);
|
||||
expect("name" in written).toBe(false);
|
||||
expect("description" in written).toBe(false);
|
||||
});
|
||||
|
||||
it("throws for a nonexistent Agent instead of initializing one", async () => {
|
||||
await expect(
|
||||
resetSystemConfigToDefaults(tmpRoot, DEFAULT_PROJECT_ID, "ghost_agent"),
|
||||
).rejects.toThrow(/not found/);
|
||||
// No directory is created as a side effect.
|
||||
await expect(
|
||||
fs.access(agentStateDir(tmpRoot, DEFAULT_PROJECT_ID, "ghost_agent")),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("project-config round trip", () => {
|
||||
it("returns default config when file is absent (without writing)", async () => {
|
||||
const cfg = await loadProjectConfig(tmpRoot, DEFAULT_PROJECT_ID);
|
||||
|
||||
Reference in new issue
Block a user