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

@@ -1,6 +1,8 @@
/**
* Agent config routes (reads/writes system_config.yaml and AGENTS.md):
* GET|PUT /api/projects/:p/agents/:a/config. Members can read and write (unrestricted).
* GET|PUT /api/projects/:p/agents/:a/config, plus POST …/config/reset to adopt the
* current default config (keeps only name/description/version — the config-side
* analogue of a skill update). Members can read and write (unrestricted).
*/
import { Hono } from "hono";
import type { AgentConfigResponse, AgentConfigUpdateRequest } from "../../api/types.js";
@@ -46,5 +48,20 @@ export function agentConfigRoutes(deps: AppDeps): Hono<AppEnv> {
} satisfies AgentConfigResponse);
});
// Overwrite system_config.yaml with the current defaults (see AgentConfigService.resetConfig
// for the exact semantics); same authorization as PUT, and responds like GET/PUT with the
// fresh config so the client can refresh in place.
app.post("/reset", async (c) => {
const projectId = requireValidId(c, "projectId");
const agentId = requireValidId(c, "agentId");
deps.projectService.requireProjectAccess(c.var.user.userId, projectId);
await deps.agentConfigService.resetConfig(projectId, agentId);
const view = await deps.agentConfigService.getConfig(projectId, agentId);
return c.json({
...view,
activeSessionCount: deps.manager.activeCountForAgent(projectId, agentId),
} satisfies AgentConfigResponse);
});
return app;
}
@@ -16,6 +16,7 @@ import {
VAULT_VALUE_MAX_LENGTH,
isValidVaultKey,
loadAgentVault,
resetSystemConfigToDefaults,
saveAgentVault,
systemConfigPath,
} from "@prismshadow/penguin-core";
@@ -173,6 +174,19 @@ export class AgentConfigService {
}
}
/**
* Overwrites system_config.yaml with the current code defaults (core's
* resetSystemConfigToDefaults) — same semantics as updating an installed skill:
* only the identity fields (name / description / version) survive; the system
* prompt, max_turns, model/compaction settings and the tool list (incl. MCP
* servers) become the current defaults. AGENTS.md, skills and the vault are
* untouched.
*/
async resetConfig(projectId: string, agentId: string): Promise<void> {
await this.requireExists(projectId, agentId);
await resetSystemConfigToDefaults(this.root, projectId, agentId);
}
private async applyConfigUpdate(
projectId: string,
agentId: string,
@@ -0,0 +1,89 @@
/**
* POST /api/projects/:projectId/agents/:agentId/config/reset — overwrites
* system_config.yaml with the current code defaults, keeping only the identity fields
* (name / description / version); the config-side analogue of a skill update. Same
* member-level authorization as PUT config; a non-member gets 404 without a write.
*/
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { defaultSystemConfig } from "@prismshadow/penguin-core";
import type { AgentConfigResponse, ProjectCreateResponse } from "../src/api/types.js";
import { apiClient, createTestApp, provisionUser } from "./helpers.js";
import type { TestApp } from "./helpers.js";
describe("POST agent config reset", () => {
let t: TestApp;
let alice: ReturnType<typeof apiClient>;
let projectId: string;
const configUrl = () => `/api/projects/${projectId}/agents/default_agent/config`;
beforeEach(async () => {
t = await createTestApp();
const a = await provisionUser(t.app, "alice");
alice = apiClient(t.app, a.cookie);
// Project ids are username-prefixed (`alice-…`), same as the other route tests.
const created = (await (
await alice.post("/api/projects", { projectId: "alice-reset", name: "Reset project" })
).json()) as ProjectCreateResponse;
projectId = created.project.projectId;
});
afterEach(async () => {
await t.cleanup();
});
it("restores the defaults while preserving name / description / version", async () => {
// Customize everything the reset is supposed to overwrite, plus the kept fields.
const put = await alice.put(configUrl(), {
config: {
name: "Custom Name",
description: "Custom description",
systemPrompt: "CUSTOM PROMPT",
maxTurns: 3,
model: { maxTokens: 1234 },
compaction: { mode: "discard" },
toolsBuiltin: [],
mcpServers: [{ name: "custom-mcp", config: {} }],
},
});
expect(put.status).toBe(200);
const res = await alice.post(`${configUrl()}/reset`);
expect(res.status).toBe(200);
const body = (await res.json()) as AgentConfigResponse;
const defaults = defaultSystemConfig();
// Identity fields survive.
expect(body.config.name).toBe("Custom Name");
expect(body.config.description).toBe("Custom description");
expect(body.config.version).toBe(1);
// Everything else is back to the current defaults.
expect(body.config.systemPrompt).toBe(defaults.system_prompt);
expect(body.config.systemPrompt).toContain("App Data Dir: {{PROJECT_DIR}}");
expect(body.config.maxTurns).toBe(defaults.max_turns);
expect(body.config.model?.maxTokens).toBe(defaults.model?.max_tokens);
expect(body.config.compaction?.mode).toBe("summarize");
expect(body.config.toolsBuiltin.map((tool) => tool.name)).toEqual(
(defaults.tools?.builtin ?? []).map((tool) => tool.name),
);
expect(body.config.mcpServers).toEqual([]);
// A later GET sees the same reset state (it was persisted, not just echoed).
const got = (await (await alice.get(configUrl())).json()) as AgentConfigResponse;
expect(got.config.systemPrompt).toBe(defaults.system_prompt);
expect(got.config.name).toBe("Custom Name");
});
it("404 for a nonexistent agent (no initialization side effect)", async () => {
const res = await alice.post(`/api/projects/${projectId}/agents/ghost/config/reset`);
expect(res.status).toBe(404);
});
it("404 for a non-member; the config is not modified", async () => {
const before = (await (await alice.get(configUrl())).json()) as AgentConfigResponse;
const m = await provisionUser(t.app, "mallory");
const mallory = apiClient(t.app, m.cookie);
const res = await mallory.post(`${configUrl()}/reset`);
expect(res.status).toBe(404);
const after = (await (await alice.get(configUrl())).json()) as AgentConfigResponse;
expect(after.systemConfigYaml).toBe(before.systemConfigYaml);
});
});