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
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user