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:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-07-26 15:08:09 +08:00
1 parent 14b5e6384f
commit 816a823b75
24 files changed
+423 -68

No files matched your search

+86 -3
View File
@@ -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);