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

@@ -73,7 +73,7 @@ const MAX_TMP_ID_ATTEMPTS = 16;
* directory name is the workspace_id, shaped like `tmp-<8hex>`; if it collides with
* an existing directory, regenerate the id. No symlinks are created inside the Workspace:
* the model composes absolute paths (to Agent State, scratchpad, etc.) directly from the
* Environment placeholders (Project Dir / Agent ID) in the system prompt.
* Environment placeholders (App Data Dir / Agent ID) in the system prompt.
*/
export async function createTempWorkspace(
root: string,
+46 -2
View File
@@ -29,6 +29,7 @@ import {
PROVIDER_PLACEHOLDER,
MODEL_ID_PLACEHOLDER,
DATE_PLACEHOLDER,
agentStateVersion,
defaultAgentsMd,
defaultSystemConfig,
OS_VERSION_PLACEHOLDER,
@@ -83,7 +84,7 @@ export interface SessionEnvironmentValues {
cwd: string;
/** The Agent id this Session belongs to (system Prompt placeholder {{AGENT_ID}}). */
agentId: string;
/** Absolute path to this Project's directory (system Prompt placeholder {{PROJECT_DIR}}; Agent State/scratchpad paths are derived from it). */
/** Absolute path to this Project's directory — PenguinHarness's app data root, injected via {{PROJECT_DIR}} and shown to the model as the Environment's "App Data Dir" line (Agent State/scratchpad paths live under its `agents/`). */
projectDir: string;
/** The session model's provider group (system Prompt placeholder {{PROVIDER}}; paired with modelId to form the model reference). */
provider: string;
@@ -212,6 +213,48 @@ export async function provisionProjectAgents(opts?: {
return agentIds;
}
/**
* Rewrites an Agent's `system_config.yaml` to the current code defaults
* (`defaultSystemConfig()`) — the config-side analogue of updating an installed Skill to
* the current library version.
*
* Exact semantics: only the identity fields of the existing file are preserved — `name`,
* `description` and the Agent State `version` (an invalid or missing version normalizes
* to 1); **everything else is replaced by the defaults**: `system_prompt`, `max_turns`,
* `model.*`, `compaction.*` (including its prompt) and `tools` (the builtin list and
* `mcpServers`). Keys outside the default schema are dropped, and YAML comments are not
* preserved (the file is rewritten from the default object). Other Agent State files
* (AGENTS.md, skills/, vault, memory/ …) are untouched. Returns the config written.
*
* Requires an existing Agent: unlike `loadOrInitAgentState` this never initializes a new
* one — it throws when `system_config.yaml` is missing.
*/
export async function resetSystemConfigToDefaults(
root: string,
projectId: string,
agentId: string,
): Promise<SystemConfig> {
// Validate before building paths, to prevent path traversal.
assertValidId("project_id", projectId);
assertValidId("agent_id", agentId);
const configPath = systemConfigPath(root, projectId, agentId);
if (!(await fileExists(configPath))) {
throw new Error(`Agent State config not found: ${configPath} (the Agent does not exist).`);
}
const parsed = parseYaml(await fs.readFile(configPath, "utf8")) as unknown;
const prev = (
parsed !== null && typeof parsed === "object" ? parsed : {}
) as Partial<SystemConfig>;
const next: SystemConfig = {
...(typeof prev.name === "string" ? { name: prev.name } : {}),
...(typeof prev.description === "string" ? { description: prev.description } : {}),
...defaultSystemConfig(),
version: agentStateVersion(prev),
};
await fs.writeFile(configPath, stringifyYaml(next), "utf8");
return next;
}
/**
* The vault key-name list: the replacement value for `{{VAULT_KEYS}}`, one `- KEY` per line;
* returns an empty string when there are no keys.
@@ -353,7 +396,8 @@ export function skillMetadataSection(skills: SkillMetadata[]): string {
* provided): this lets the model know which APIs requiring a key it can call; values are never
* injected. `{{SKILL_METADATA}}` is replaced with the installed Skills' metadata lines (an empty
* string if empty/not provided). A custom template that removes a placeholder gets no
* corresponding content injected.
* corresponding content injected. `{{PROJECT_DIR}}` resolves to the Project directory —
* the app data root the default prompt labels "App Data Dir".
* Docs: /docs/configuration § "System prompt placeholders".
*/
export function assembleSystemPrompt(
+11 -9
View File
@@ -14,7 +14,7 @@
*
* Placeholders (`{{...}}`) appear only in the trailing injection zones (AGENTS.md / Vault /
* Skills / Environment); elsewhere the body uses angle-bracket notation such as
* \`<project_dir>\`, \`<agent_id>\`, \`<session_id>\` — these are **not substituted**; the model
* \`<app_data_dir>\`, \`<agent_id>\`, \`<session_id>\` — these are **not substituted**; the model
* fills in the actual values from the Environment section itself.
*/
import type { MCPServerConfig, ThinkingLevelName, ToolDefinitionConfig } from "../interfaces.js";
@@ -27,6 +27,7 @@ export const SKILL_METADATA_PLACEHOLDER = "{{SKILL_METADATA}}";
export const SESSION_ID_PLACEHOLDER = "{{SESSION_ID}}";
export const CWD_PLACEHOLDER = "{{CWD}}";
export const AGENT_ID_PLACEHOLDER = "{{AGENT_ID}}";
/** The Project directory — PenguinHarness's app data root, exposed in the default prompt as the "App Data Dir" Environment line (deliberately not called a project/task directory there). */
export const PROJECT_DIR_PLACEHOLDER = "{{PROJECT_DIR}}";
export const PROVIDER_PLACEHOLDER = "{{PROVIDER}}";
export const MODEL_ID_PLACEHOLDER = "{{MODEL_ID}}";
@@ -113,17 +114,18 @@ Some user-side messages are system-synthesized records, not user text to answer
- \`<context_summary>\`: earlier conversation was compacted. This summary replaced the raw transcript and is its only record; treat it as established context and continue the task from it.
# File system
- Angle-bracket markers such as \`<project_dir>\`, \`<agent_id>\` and \`<session_id>\` are not literal paths — substitute the matching values from the Environment section.
- Angle-bracket markers such as \`<app_data_dir>\`, \`<agent_id>\` and \`<session_id>\` are not literal paths — substitute the matching values from the Environment section.
- You run inside the user's working folder (\`CWD\` in Environment).
- The project directory is \`<project_dir>\`; every agent of this project lives under \`<project_dir>/agents/\`, so another agent's assets are at \`<project_dir>/agents/<its_agent_id>/agent_state/\`.
- Your own Agent State is \`<project_dir>/agents/<agent_id>/agent_state/\` — it holds your assets such as \`skills/\`, and its \`AGENTS.md\` is already included in your context. Reach these paths directly.
- For temporary and scratch files, create a subdirectory named after the current Session ID under your scratchpad: \`<project_dir>/agents/<agent_id>/scratchpad/<session_id>/\`. Build intermediates there, but always place final deliverables in the workspace (under \`CWD\`) — files left in the scratchpad are not part of your output.
- The App Data Dir is PenguinHarness's application data root: it holds every agent's data files (\`<app_data_dir>/agents/<agent_id>/agent_state/\` and friends) plus the project-level data files. It is NOT the current task's directory and was not provided by the user — never treat its contents as task input, and never place task deliverables there; the working folder is \`CWD\`.
- Another agent's assets are at \`<app_data_dir>/agents/<its_agent_id>/agent_state/\`.
- Your own Agent State is \`<app_data_dir>/agents/<agent_id>/agent_state/\` — it holds your assets such as \`skills/\`, and its \`AGENTS.md\` is already included in your context. Reach these paths directly.
- For temporary and scratch files, create a subdirectory named after the current Session ID under your scratchpad: \`<app_data_dir>/agents/<agent_id>/scratchpad/<session_id>/\`. Build intermediates there, but always place final deliverables in the workspace (under \`CWD\`) — files left in the scratchpad are not part of your output.
- When you create or update a file in the workspace, mention its workspace-relative path in backticks (e.g. \`src/app.py\`) in your reply, so the user can open it from the message.
- Never read, copy, print or otherwise access \`<project_dir>/.project_config.toml\` or any agent's \`agent_state/.vault.toml\` — they hold the user's API keys and other secrets, which are none of your business. Configuration is CLI-only: change models or credentials with \`penguin config ...\` commands. If a task seems to require these files, say so and ask the user instead.
- Never read, copy, print or otherwise access \`.project_config.toml\` directly under the App Data Dir, or any agent's \`agent_state/.vault.toml\` — they hold the user's API keys and other secrets, which are none of your business. Configuration is CLI-only: change models or credentials with \`penguin config ...\` commands. If a task seems to require these files, say so and ask the user instead.
# Suggested workflows
These are recommendations, not requirements; adapt them as the task demands.
- For a long-horizon task, first write a plan in Markdown to \`<project_dir>/agents/<agent_id>/scratchpad/<session_id>/PLAN.md\`, containing a task overview and an itemized step-by-step plan; update it after each completed step to keep execution consistent.
- For a long-horizon task, first write a plan in Markdown to \`<app_data_dir>/agents/<agent_id>/scratchpad/<session_id>/PLAN.md\`, containing a task overview and an itemized step-by-step plan; update it after each completed step to keep execution consistent.
- Delegate self-contained subtasks to other agents with the \`run_subagent\` tool; dispatch independent subtasks in parallel. Start every delegation prompt with your own agent id (e.g. "Caller agent: <agent_id>") and name the skill the subagent should use when the task matches one. Subagents share your Workspace — exchange data through files. If \`run_subagent\` is not in your tool list, you are the subagent: do the work yourself.
- To visit web pages, prefer Playwright when installed; otherwise \`curl\`. When building a web app or frontend, prefer React.
@@ -138,14 +140,14 @@ The vault holds this agent's per-agent secrets (agent_state/.vault.toml). Each e
{{VAULT_KEYS}}
# Skills
Skills are reusable instruction packages stored under <project_dir>/agents/<agent_id>/agent_state/skills/<skill_name>/SKILL.md. There is no skill tool: when a task matches an installed skill below, or the user asks to use one (a message may start with a <use_skills> block listing skill names), first read that skill's SKILL.md in full with a shell command, then follow it. If a request only names a skill without a concrete task, ask the user what they need before starting.
Skills are reusable instruction packages stored under <app_data_dir>/agents/<agent_id>/agent_state/skills/<skill_name>/SKILL.md. There is no skill tool: when a task matches an installed skill below, or the user asks to use one (a message may start with a <use_skills> block listing skill names), first read that skill's SKILL.md in full with a shell command, then follow it. If a request only names a skill without a concrete task, ask the user what they need before starting.
{{SKILL_METADATA}}
# Environment
- Platform: {{PLATFORM}}
- OS Version: {{OS_VERSION}}
- Date: {{DATE}}
- Project Dir: {{PROJECT_DIR}}
- App Data Dir: {{PROJECT_DIR}}
- Agent ID: {{AGENT_ID}}
- CWD: {{CWD}}
- Provider: {{PROVIDER}}
+9 -1
View File
@@ -29,9 +29,17 @@ export function projectDir(root: string, projectId: string): string {
return path.join(root, projectId);
}
/**
* `<projectDir>/agents` from an already-resolved Project directory path — the single
* definition point of the agents-container layout.
*/
export function agentsDirFrom(projectDirPath: string): string {
return path.join(projectDirPath, "agents");
}
/** `<projectDir>/agents`, the container directory holding every Agent in the Project. */
export function agentsDir(root: string, projectId: string): string {
return path.join(projectDir(root, projectId), "agents");
return agentsDirFrom(projectDir(root, projectId));
}
/** `<projectDir>/agents/<agentId>`. */