mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
feat: enable permission-aware subagents
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* prompts.ts — System prompt builder for agents.
|
||||
*/
|
||||
|
||||
import type { AgentConfig, EnvInfo } from "./types.js";
|
||||
|
||||
/** Extra sections to inject into the system prompt (memory, skills, etc.). */
|
||||
export interface PromptExtras {
|
||||
/** Persistent memory content to inject (first 200 lines of MEMORY.md + instructions). */
|
||||
memoryBlock?: string;
|
||||
/** Preloaded skill contents to inject. */
|
||||
skillBlocks?: { name: string; content: string }[];
|
||||
/**
|
||||
* Parent directory the worktree copy was created from. Set only for
|
||||
* `isolation: "worktree"` spawns — triggers the block that tells the agent
|
||||
* to stay in the copy.
|
||||
*/
|
||||
worktreeBase?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the system prompt for an agent from its config.
|
||||
*
|
||||
* - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
|
||||
* - "append" mode: parent system prompt + sub-agent context + env header + config.systemPrompt
|
||||
* - "append" with empty systemPrompt: pure parent clone
|
||||
*
|
||||
* Both modes include an `<active_agent name="${config.name}"/>` tag so downstream
|
||||
* extensions (e.g. permission/policy systems) can resolve per-agent policy
|
||||
* inside the child session by parsing the system prompt. In replace mode the tag
|
||||
* is prepended; in append mode it follows the shared inherited content so the
|
||||
* parent prompt forms an identical, cacheable byte prefix with the parent
|
||||
* session (the LLM's KV cache can then reuse those tokens across every spawn).
|
||||
*
|
||||
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
||||
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
||||
*/
|
||||
export function buildAgentPrompt(
|
||||
config: AgentConfig,
|
||||
cwd: string,
|
||||
env: EnvInfo,
|
||||
parentSystemPrompt?: string,
|
||||
extras?: PromptExtras,
|
||||
): string {
|
||||
const activeAgentTag = `<active_agent name="${config.name}"/>\n\n`;
|
||||
|
||||
const envBlock = `# Environment
|
||||
Working directory: ${cwd}
|
||||
${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
|
||||
Platform: ${env.platform}`;
|
||||
|
||||
// A worktree agent is told its cwd twice: by the env block above (the copy)
|
||||
// and by whatever names the main checkout — the inherited parent prompt in
|
||||
// append mode, or the task prompt in either mode. It follows the latter and
|
||||
// works in the shared tree (#187), so resolve the contradiction explicitly.
|
||||
const worktreeBlock = extras?.worktreeBase
|
||||
? `\n\n<worktree_isolation>
|
||||
Your working directory is an isolated git worktree copy of ${extras.worktreeBase}.
|
||||
Work only inside it — never in ${extras.worktreeBase}, even if other instructions name that path as your working directory.
|
||||
</worktree_isolation>`
|
||||
: "";
|
||||
|
||||
// Build optional extras suffix
|
||||
const extraSections: string[] = [];
|
||||
if (extras?.memoryBlock) {
|
||||
extraSections.push(extras.memoryBlock);
|
||||
}
|
||||
if (extras?.skillBlocks?.length) {
|
||||
for (const skill of extras.skillBlocks) {
|
||||
extraSections.push(`\n# Preloaded Skill: ${skill.name}\n${skill.content}`);
|
||||
}
|
||||
}
|
||||
const extrasSuffix = extraSections.length > 0 ? "\n\n" + extraSections.join("\n") : "";
|
||||
|
||||
if (config.promptMode === "append") {
|
||||
const identity = parentSystemPrompt || genericBase;
|
||||
|
||||
const bridge = `<sub_agent_context>
|
||||
You are operating as a sub-agent invoked to handle a specific task.
|
||||
- Use the read tool instead of cat/head/tail
|
||||
- Use the edit tool instead of sed/awk
|
||||
- Use the write tool instead of echo/heredoc
|
||||
- Use the find tool instead of bash find/ls for file search
|
||||
- Use the grep tool instead of bash grep/rg for content search
|
||||
- Make independent tool calls in parallel
|
||||
- Use absolute file paths
|
||||
- Do not use emojis
|
||||
- Be concise but complete
|
||||
</sub_agent_context>`;
|
||||
|
||||
const customSection = config.systemPrompt?.trim()
|
||||
? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
|
||||
: "";
|
||||
|
||||
// Place shared/stable content first so the LLM's KV cache can reuse the
|
||||
// inherited prefix across all subagent invocations. The parent prompt is
|
||||
// placed verbatim (no wrapper tag) so it forms an identical byte prefix
|
||||
// with the parent session, maximising KV cache hits. The <active_agent>
|
||||
// tag and env block vary per call and are placed after the cached prefix.
|
||||
return identity + "\n\n" + bridge + "\n\n" + activeAgentTag + envBlock + worktreeBlock + customSection + extrasSuffix;
|
||||
}
|
||||
|
||||
// "replace" mode — env header + the config's full system prompt
|
||||
const replaceHeader = `You are a pi coding agent sub-agent.
|
||||
You have been invoked to handle a specific task autonomously.
|
||||
|
||||
${envBlock}`;
|
||||
|
||||
return activeAgentTag + replaceHeader + worktreeBlock + "\n\n" + config.systemPrompt + extrasSuffix;
|
||||
}
|
||||
|
||||
/** Fallback base prompt when parent system prompt is unavailable in append mode. */
|
||||
const genericBase = `# Role
|
||||
You are a general-purpose coding agent for complex, multi-step tasks.
|
||||
You have full access to read, write, edit files, and execute commands.
|
||||
Do what has been asked; nothing more, nothing less.`;
|
||||
Reference in New Issue
Block a user