@gotgenes/pi-subagents 6.19.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/docs/architecture/architecture.md +193 -95
- package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
- package/docs/plans/0185-remove-persistent-agent-memory.md +161 -0
- package/docs/plans/0192-define-session-context-interface.md +107 -0
- package/docs/retro/0185-remove-persistent-agent-memory.md +73 -0
- package/docs/retro/0188-replace-any-casts-with-sdk-types.md +29 -0
- package/docs/retro/0192-define-session-context-interface.md +35 -0
- package/package.json +1 -1
- package/src/config/agent-types.ts +0 -20
- package/src/config/custom-agents.ts +1 -11
- package/src/index.ts +1 -3
- package/src/session/prompts.ts +1 -6
- package/src/session/safe-fs.ts +45 -0
- package/src/session/session-config.ts +3 -49
- package/src/session/skill-loader.ts +1 -1
- package/src/types.ts +21 -5
- package/src/ui/agent-config-editor.ts +0 -1
- package/src/ui/agent-creation-wizard.ts +0 -1
- package/src/session/memory.ts +0 -168
package/src/types.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
6
6
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
export { AgentRecord } from "#src/lifecycle/agent-record";
|
|
@@ -20,9 +21,6 @@ export interface SubscribableSession {
|
|
|
20
21
|
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
21
22
|
export type SubagentType = string;
|
|
22
23
|
|
|
23
|
-
/** Memory scope for persistent agent memory. */
|
|
24
|
-
export type MemoryScope = "user" | "project" | "local";
|
|
25
|
-
|
|
26
24
|
/** Isolation mode for agent execution. */
|
|
27
25
|
export type IsolationMode = "worktree";
|
|
28
26
|
|
|
@@ -59,8 +57,6 @@ export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
|
|
|
59
57
|
runInBackground?: boolean;
|
|
60
58
|
/** Default for spawn: no extension tools. undefined = caller decides. */
|
|
61
59
|
isolated?: boolean;
|
|
62
|
-
/** Persistent memory scope — agents with memory get a persistent directory and MEMORY.md */
|
|
63
|
-
memory?: MemoryScope;
|
|
64
60
|
/** Isolation mode — "worktree" runs the agent in a temporary git worktree */
|
|
65
61
|
isolation?: IsolationMode;
|
|
66
62
|
/** true = this is an embedded default agent (informational) */
|
|
@@ -82,6 +78,26 @@ export interface AgentInvocation {
|
|
|
82
78
|
isolation?: IsolationMode;
|
|
83
79
|
}
|
|
84
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Narrow shell-exec callback replacing `ExtensionAPI` in `detectEnv()`.
|
|
83
|
+
* Matches the shape of `pi.exec()` without carrying an SDK dependency.
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* Narrow interface capturing the ExtensionContext fields SubagentRuntime needs.
|
|
87
|
+
* Avoids coupling runtime to the full SDK ExtensionContext surface (ISP).
|
|
88
|
+
*/
|
|
89
|
+
export interface SessionContext {
|
|
90
|
+
readonly cwd: string;
|
|
91
|
+
readonly model: unknown;
|
|
92
|
+
readonly modelRegistry: ModelRegistry | undefined;
|
|
93
|
+
getSystemPrompt(): string;
|
|
94
|
+
readonly sessionManager: {
|
|
95
|
+
getSessionFile(): string | undefined;
|
|
96
|
+
getSessionId(): string;
|
|
97
|
+
getBranch(): unknown[];
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
/**
|
|
86
102
|
* Narrow shell-exec callback replacing `ExtensionAPI` in `detectEnv()`.
|
|
87
103
|
* Matches the shape of `pi.exec()` without carrying an SDK dependency.
|
|
@@ -131,7 +131,6 @@ export function createAgentConfigEditor(
|
|
|
131
131
|
if (cfg.inheritContext) fmFields.push("inherit_context: true");
|
|
132
132
|
if (cfg.runInBackground) fmFields.push("run_in_background: true");
|
|
133
133
|
if (cfg.isolated) fmFields.push("isolated: true");
|
|
134
|
-
if (cfg.memory) fmFields.push(`memory: ${cfg.memory}`);
|
|
135
134
|
if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
|
|
136
135
|
|
|
137
136
|
const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
@@ -115,7 +115,6 @@ disallowed_tools: <comma-separated tool names to block, even if otherwise availa
|
|
|
115
115
|
inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
|
|
116
116
|
run_in_background: <true to run in background by default. Default: false>
|
|
117
117
|
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
118
|
-
memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>
|
|
119
118
|
isolation: <"worktree" to run in isolated git worktree. Omit for normal>
|
|
120
119
|
---
|
|
121
120
|
|
package/src/session/memory.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* memory.ts — Persistent agent memory: per-agent memory directories that persist across sessions.
|
|
3
|
-
*
|
|
4
|
-
* Memory scopes:
|
|
5
|
-
* - "user" → ~/.pi/agent-memory/{agent-name}/
|
|
6
|
-
* - "project" → .pi/agent-memory/{agent-name}/
|
|
7
|
-
* - "local" → .pi/agent-memory-local/{agent-name}/
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { join, } from "node:path";
|
|
13
|
-
import { debugLog } from "#src/debug";
|
|
14
|
-
import type { MemoryScope } from "#src/types";
|
|
15
|
-
|
|
16
|
-
/** Maximum lines to read from MEMORY.md */
|
|
17
|
-
const MAX_MEMORY_LINES = 200;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Returns true if a name contains characters not allowed in agent/skill names.
|
|
21
|
-
* Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
|
|
22
|
-
*/
|
|
23
|
-
export function isUnsafeName(name: string): boolean {
|
|
24
|
-
if (!name || name.length > 128) return true;
|
|
25
|
-
return !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Returns true if the given path is a symlink (defense against symlink attacks).
|
|
30
|
-
*/
|
|
31
|
-
export function isSymlink(filePath: string): boolean {
|
|
32
|
-
try {
|
|
33
|
-
return lstatSync(filePath).isSymbolicLink();
|
|
34
|
-
} catch (err) {
|
|
35
|
-
debugLog("lstatSync", err);
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Safely read a file, rejecting symlinks.
|
|
42
|
-
* Returns undefined if the file doesn't exist, is a symlink, or can't be read.
|
|
43
|
-
*/
|
|
44
|
-
export function safeReadFile(filePath: string): string | undefined {
|
|
45
|
-
if (!existsSync(filePath)) return undefined;
|
|
46
|
-
if (isSymlink(filePath)) return undefined;
|
|
47
|
-
try {
|
|
48
|
-
return readFileSync(filePath, "utf-8");
|
|
49
|
-
} catch (err) {
|
|
50
|
-
debugLog("readFileSync", err);
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Resolve the memory directory path for a given agent + scope + cwd.
|
|
57
|
-
* Throws if agentName contains path traversal characters.
|
|
58
|
-
*/
|
|
59
|
-
export function resolveMemoryDir(agentName: string, scope: MemoryScope, cwd: string): string {
|
|
60
|
-
if (isUnsafeName(agentName)) {
|
|
61
|
-
throw new Error(`Unsafe agent name for memory directory: "${agentName}"`);
|
|
62
|
-
}
|
|
63
|
-
switch (scope) {
|
|
64
|
-
case "user":
|
|
65
|
-
return join(homedir(), ".pi", "agent-memory", agentName);
|
|
66
|
-
case "project":
|
|
67
|
-
return join(cwd, ".pi", "agent-memory", agentName);
|
|
68
|
-
case "local":
|
|
69
|
-
return join(cwd, ".pi", "agent-memory-local", agentName);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Ensure the memory directory exists, creating it if needed.
|
|
75
|
-
* Refuses to create directories if any component in the path is a symlink
|
|
76
|
-
* to prevent symlink-based directory traversal attacks.
|
|
77
|
-
*/
|
|
78
|
-
export function ensureMemoryDir(memoryDir: string): void {
|
|
79
|
-
// If the directory already exists, verify it's not a symlink
|
|
80
|
-
if (existsSync(memoryDir)) {
|
|
81
|
-
if (isSymlink(memoryDir)) {
|
|
82
|
-
throw new Error(`Refusing to use symlinked memory directory: ${memoryDir}`);
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
mkdirSync(memoryDir, { recursive: true });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Read the first N lines of MEMORY.md from the memory directory, if it exists.
|
|
91
|
-
* Returns undefined if no MEMORY.md exists or if the path is a symlink.
|
|
92
|
-
*/
|
|
93
|
-
export function readMemoryIndex(memoryDir: string): string | undefined {
|
|
94
|
-
// Reject symlinked memory directories
|
|
95
|
-
if (isSymlink(memoryDir)) return undefined;
|
|
96
|
-
|
|
97
|
-
const memoryFile = join(memoryDir, "MEMORY.md");
|
|
98
|
-
const content = safeReadFile(memoryFile);
|
|
99
|
-
if (content === undefined) return undefined;
|
|
100
|
-
|
|
101
|
-
const lines = content.split("\n");
|
|
102
|
-
if (lines.length > MAX_MEMORY_LINES) {
|
|
103
|
-
return lines.slice(0, MAX_MEMORY_LINES).join("\n") + "\n... (truncated at 200 lines)";
|
|
104
|
-
}
|
|
105
|
-
return content;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Build the memory block to inject into the agent's system prompt.
|
|
110
|
-
* Also ensures the memory directory exists (creates it if needed).
|
|
111
|
-
*/
|
|
112
|
-
export function buildMemoryBlock(agentName: string, scope: MemoryScope, cwd: string): string {
|
|
113
|
-
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
114
|
-
// Create the memory directory so the agent can immediately write to it
|
|
115
|
-
ensureMemoryDir(memoryDir);
|
|
116
|
-
|
|
117
|
-
const existingMemory = readMemoryIndex(memoryDir);
|
|
118
|
-
|
|
119
|
-
const header = `# Agent Memory
|
|
120
|
-
|
|
121
|
-
You have a persistent memory directory at: ${memoryDir}/
|
|
122
|
-
Memory scope: ${scope}
|
|
123
|
-
|
|
124
|
-
This memory persists across sessions. Use it to build up knowledge over time.`;
|
|
125
|
-
|
|
126
|
-
const memoryContent = existingMemory
|
|
127
|
-
? `\n\n## Current MEMORY.md\n${existingMemory}`
|
|
128
|
-
: `\n\nNo MEMORY.md exists yet. Create one at ${join(memoryDir, "MEMORY.md")} to start building persistent memory.`;
|
|
129
|
-
|
|
130
|
-
const instructions = `
|
|
131
|
-
|
|
132
|
-
## Memory Instructions
|
|
133
|
-
- MEMORY.md is an index file — keep it concise (under 200 lines). Lines after 200 are truncated.
|
|
134
|
-
- Store detailed memories in separate files within ${memoryDir}/ and link to them from MEMORY.md.
|
|
135
|
-
- Each memory file should use this frontmatter format:
|
|
136
|
-
\`\`\`markdown
|
|
137
|
-
---
|
|
138
|
-
name: <memory name>
|
|
139
|
-
description: <one-line description>
|
|
140
|
-
type: <user|feedback|project|reference>
|
|
141
|
-
---
|
|
142
|
-
<memory content>
|
|
143
|
-
\`\`\`
|
|
144
|
-
- Update or remove memories that become outdated. Check for existing memories before creating duplicates.
|
|
145
|
-
- You have Read, Write, and Edit tools available for managing memory files.`;
|
|
146
|
-
|
|
147
|
-
return header + memoryContent + instructions;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Build a read-only memory block for agents that lack write/edit tools.
|
|
152
|
-
* Does NOT create the memory directory — agents can only consume existing memory.
|
|
153
|
-
*/
|
|
154
|
-
export function buildReadOnlyMemoryBlock(agentName: string, scope: MemoryScope, cwd: string): string {
|
|
155
|
-
const memoryDir = resolveMemoryDir(agentName, scope, cwd);
|
|
156
|
-
const existingMemory = readMemoryIndex(memoryDir);
|
|
157
|
-
|
|
158
|
-
const header = `# Agent Memory (read-only)
|
|
159
|
-
|
|
160
|
-
Memory scope: ${scope}
|
|
161
|
-
You have read-only access to memory. You can reference existing memories but cannot create or modify them.`;
|
|
162
|
-
|
|
163
|
-
const memoryContent = existingMemory
|
|
164
|
-
? `\n\n## Current MEMORY.md\n${existingMemory}`
|
|
165
|
-
: `\n\nNo memory is available yet. Other agents or sessions with write access can create memories for you to consume.`;
|
|
166
|
-
|
|
167
|
-
return header + memoryContent;
|
|
168
|
-
}
|