@yandy0725/pi-memory 1.2.0 → 1.3.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/README.md +20 -6
- package/README.zh.md +20 -6
- package/index.ts +19 -9
- package/package.json +1 -1
- package/src/agent-runner.ts +22 -12
- package/src/config.ts +29 -3
- package/src/dream.ts +3 -1
- package/src/extract.ts +3 -1
- package/src/inject.ts +9 -16
package/README.md
CHANGED
|
@@ -44,11 +44,16 @@ Create `memory.json` in the agent directory (`~/.pi/agent/memory.json`) or the p
|
|
|
44
44
|
"memoryDir": "~/.pi/memory",
|
|
45
45
|
"memIndexMaxLines": 200,
|
|
46
46
|
"memIndexMaxBytes": 25600,
|
|
47
|
+
"defaults": {
|
|
48
|
+
"model": "deepseek/deepseek-v4-flash",
|
|
49
|
+
"sessionPersistence": { "enabled": false }
|
|
50
|
+
},
|
|
47
51
|
"dream": {
|
|
48
52
|
"nudgeAfterSessions": 5,
|
|
49
53
|
"nudgeAfterHours": 24,
|
|
50
54
|
"model": "auto",
|
|
51
|
-
"thinkLevel": "high"
|
|
55
|
+
"thinkLevel": "high",
|
|
56
|
+
"sessionPersistence": { "enabled": false }
|
|
52
57
|
},
|
|
53
58
|
"sessionSearch": {
|
|
54
59
|
"maxSessions": 10,
|
|
@@ -60,13 +65,15 @@ Create `memory.json` in the agent directory (`~/.pi/agent/memory.json`) or the p
|
|
|
60
65
|
"thinkLevel": "off",
|
|
61
66
|
"maxFiles": 5,
|
|
62
67
|
"maxTopicBytes": 4096,
|
|
63
|
-
"maxInjectionBytes": 20480
|
|
68
|
+
"maxInjectionBytes": 20480,
|
|
69
|
+
"sessionPersistence": { "enabled": false }
|
|
64
70
|
},
|
|
65
71
|
"extractMemories": {
|
|
66
72
|
"enabled": true,
|
|
67
73
|
"model": "auto",
|
|
68
74
|
"thinkLevel": "high",
|
|
69
|
-
"maxContextTokens": 2000
|
|
75
|
+
"maxContextTokens": 2000,
|
|
76
|
+
"sessionPersistence": { "enabled": false }
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
```
|
|
@@ -77,21 +84,28 @@ Create `memory.json` in the agent directory (`~/.pi/agent/memory.json`) or the p
|
|
|
77
84
|
| `memoryDir` | `~/.pi/memory` | Root directory for all memory data |
|
|
78
85
|
| `memIndexMaxLines` | `200` | Max lines in `MEMORY.md` before capacity errors |
|
|
79
86
|
| `memIndexMaxBytes` | `25600` | Max bytes in `MEMORY.md` before capacity errors |
|
|
87
|
+
| `defaults.model` | — | Shared model fallback for all sub-tasks. Per-task `model` overrides |
|
|
88
|
+
| `defaults.sessionPersistence.enabled` | `false` | Shared session persistence fallback (default: in-memory). Per-task overrides |
|
|
89
|
+
| `defaults.sessionPersistence.sessionDir` | `memoryDir/sessions/` | Custom session directory for persisted headless agent sessions |
|
|
80
90
|
| `dream.nudgeAfterSessions` | `5` | Sessions since last dream before nudge is shown |
|
|
81
91
|
| `dream.nudgeAfterHours` | `24` | Hours since last dream before nudge is shown |
|
|
82
|
-
| `dream.model` |
|
|
92
|
+
| `dream.model` | — | Model for dream consolidation (`"provider/id"`). Falls back to `defaults.model` → parent model |
|
|
83
93
|
| `dream.thinkLevel` | `"high"` | Thinking effort for dream subagent: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` |
|
|
94
|
+
| `dream.sessionPersistence.enabled` | `false` | Persist dream agent sessions to disk (debug/audit). Falls back to `defaults.sessionPersistence.enabled` |
|
|
95
|
+
| `dream.sessionPersistence.sessionDir` | `memoryDir/sessions/` | Custom session directory for dream sessions |
|
|
84
96
|
| `sessionSearch.maxSessions` | `10` | Max sessions to scan when searching history |
|
|
85
97
|
| `sessionSearch.maxMatches` | `5` | Max matches to return from history search |
|
|
86
98
|
| `autoSurfacing.enabled` | `true` | ⭐ Enable per-turn topic file auto-injection |
|
|
87
|
-
| `autoSurfacing.model` |
|
|
99
|
+
| `autoSurfacing.model` | — | ⭐ Model for side-query relevance selection. Falls back to `defaults.model` → parent model |
|
|
88
100
|
| `autoSurfacing.thinkLevel` | `"off"` | ⭐ Thinking effort for side-query (recommended: `"off"` for lightweight selection) |
|
|
101
|
+
| `autoSurfacing.sessionPersistence.enabled` | `false` | Persist side-query agent sessions to disk. Falls back to `defaults.sessionPersistence.enabled` |
|
|
89
102
|
| `autoSurfacing.maxFiles` | `5` | ⭐ Max topic files to inject per turn |
|
|
90
103
|
| `autoSurfacing.maxTopicBytes` | `4096` | ⭐ Max bytes per injected topic file (truncated) |
|
|
91
104
|
| `autoSurfacing.maxInjectionBytes` | `20480` | ⭐ Max total bytes of injected content per turn |
|
|
92
105
|
| `extractMemories.enabled` | `true` | ⭐ Enable per-turn memory extraction |
|
|
93
|
-
| `extractMemories.model` |
|
|
106
|
+
| `extractMemories.model` | — | ⭐ Model for the extraction subagent. Falls back to `defaults.model` → parent model |
|
|
94
107
|
| `extractMemories.thinkLevel` | `"high"` | ⭐ Thinking effort for extraction: `"off"` / `"minimal"` / `"low"` / `"medium"` / `"high"` / `"xhigh"` |
|
|
108
|
+
| `extractMemories.sessionPersistence.enabled` | `false` | Persist extract agent sessions to disk. Falls back to `defaults.sessionPersistence.enabled` |
|
|
95
109
|
| `extractMemories.maxContextTokens` | `2000` | ⭐ Max tokens of conversation to analyze |
|
|
96
110
|
|
|
97
111
|
Project-level config (`.pi/memory.json`) is only loaded when the project is trusted.
|
package/README.zh.md
CHANGED
|
@@ -44,11 +44,16 @@ pi install npm:@yandy0725/pi-memory
|
|
|
44
44
|
"memoryDir": "~/.pi/memory",
|
|
45
45
|
"memIndexMaxLines": 200,
|
|
46
46
|
"memIndexMaxBytes": 25600,
|
|
47
|
+
"defaults": {
|
|
48
|
+
"model": "deepseek/deepseek-v4-flash",
|
|
49
|
+
"sessionPersistence": { "enabled": false }
|
|
50
|
+
},
|
|
47
51
|
"dream": {
|
|
48
52
|
"nudgeAfterSessions": 5,
|
|
49
53
|
"nudgeAfterHours": 24,
|
|
50
54
|
"model": "auto",
|
|
51
|
-
"thinkLevel": "high"
|
|
55
|
+
"thinkLevel": "high",
|
|
56
|
+
"sessionPersistence": { "enabled": false }
|
|
52
57
|
},
|
|
53
58
|
"sessionSearch": {
|
|
54
59
|
"maxSessions": 10,
|
|
@@ -60,13 +65,15 @@ pi install npm:@yandy0725/pi-memory
|
|
|
60
65
|
"thinkLevel": "off",
|
|
61
66
|
"maxFiles": 5,
|
|
62
67
|
"maxTopicBytes": 4096,
|
|
63
|
-
"maxInjectionBytes": 20480
|
|
68
|
+
"maxInjectionBytes": 20480,
|
|
69
|
+
"sessionPersistence": { "enabled": false }
|
|
64
70
|
},
|
|
65
71
|
"extractMemories": {
|
|
66
72
|
"enabled": true,
|
|
67
73
|
"model": "auto",
|
|
68
74
|
"thinkLevel": "high",
|
|
69
|
-
"maxContextTokens": 2000
|
|
75
|
+
"maxContextTokens": 2000,
|
|
76
|
+
"sessionPersistence": { "enabled": false }
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
```
|
|
@@ -77,21 +84,28 @@ pi install npm:@yandy0725/pi-memory
|
|
|
77
84
|
| `memoryDir` | `~/.pi/memory` | 所有记忆数据的根目录 |
|
|
78
85
|
| `memIndexMaxLines` | `200` | `MEMORY.md` 最大行数 |
|
|
79
86
|
| `memIndexMaxBytes` | `25600` | `MEMORY.md` 最大字节数 |
|
|
87
|
+
| `defaults.model` | — | 所有子任务共享的模型回退值。per-task `model` 会覆盖 |
|
|
88
|
+
| `defaults.sessionPersistence.enabled` | `false` | 共享的 session 持久化回退(默认不持久化)。per-task 可覆盖 |
|
|
89
|
+
| `defaults.sessionPersistence.sessionDir` | `memoryDir/sessions/` | 自定义持久化目录 |
|
|
80
90
|
| `dream.nudgeAfterSessions` | `5` | 触发提醒需经过的会话数 |
|
|
81
91
|
| `dream.nudgeAfterHours` | `24` | 触发提醒需经过的小时数 |
|
|
82
|
-
| `dream.model` |
|
|
92
|
+
| `dream.model` | — | 整理使用的模型(`"provider/id"`)。回退链:per-task → `defaults.model` → 父模型 |
|
|
83
93
|
| `dream.thinkLevel` | `"high"` | 整理子 agent 的思考深度:`"off"` / `"minimal"` / `"low"` / `"medium"` / `"high"` / `"xhigh"` |
|
|
94
|
+
| `dream.sessionPersistence.enabled` | `false` | 持久化 dream agent 的 session 到磁盘(调试/审计)。回退到 `defaults.sessionPersistence.enabled` |
|
|
95
|
+
| `dream.sessionPersistence.sessionDir` | `memoryDir/sessions/` | dream session 自定义目录 |
|
|
84
96
|
| `sessionSearch.maxSessions` | `10` | 搜索历史时最多扫描的会话数 |
|
|
85
97
|
| `sessionSearch.maxMatches` | `5` | 历史搜索最多返回的匹配数 |
|
|
86
98
|
| `autoSurfacing.enabled` | `true` | ⭐ 启用 per-turn topic 文件自动注入 |
|
|
87
|
-
| `autoSurfacing.model` |
|
|
99
|
+
| `autoSurfacing.model` | — | ⭐ side-query 相关性选择模型。回退链:per-task → `defaults.model` → 父模型 |
|
|
88
100
|
| `autoSurfacing.thinkLevel` | `"off"` | ⭐ side-query 思考深度(推荐 `"off"`,轻量选择任务) |
|
|
101
|
+
| `autoSurfacing.sessionPersistence.enabled` | `false` | 持久化 side-query agent 的 session。回退到 `defaults.sessionPersistence.enabled` |
|
|
89
102
|
| `autoSurfacing.maxFiles` | `5` | ⭐ 每轮最多注入的 topic 文件数 |
|
|
90
103
|
| `autoSurfacing.maxTopicBytes` | `4096` | ⭐ 单个注入 topic 文件最大字节数(截断) |
|
|
91
104
|
| `autoSurfacing.maxInjectionBytes` | `20480` | ⭐ 每轮注入内容总字节数上限 |
|
|
92
105
|
| `extractMemories.enabled` | `true` | ⭐ 启用 per-turn 记忆自动提取 |
|
|
93
|
-
| `extractMemories.model` |
|
|
106
|
+
| `extractMemories.model` | — | ⭐ 提取子 agent 使用的模型。回退链:per-task → `defaults.model` → 父模型 |
|
|
94
107
|
| `extractMemories.thinkLevel` | `"high"` | ⭐ 提取的思考深度:`"off"` / `"minimal"` / `"low"` / `"medium"` / `"high"` / `"xhigh"` |
|
|
108
|
+
| `extractMemories.sessionPersistence.enabled` | `false` | 持久化 extract agent 的 session。回退到 `defaults.sessionPersistence.enabled` |
|
|
95
109
|
| `extractMemories.maxContextTokens` | `2000` | ⭐ 分析对话的最大 token 数 |
|
|
96
110
|
|
|
97
111
|
项目级配置(`.pi/memory.json`)仅在项目受信任时加载。
|
package/index.ts
CHANGED
|
@@ -2,12 +2,11 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { loadConfig, type MemoryConfig } from "./src/config";
|
|
5
|
+
import { loadConfig, type MemoryConfig, type SessionPersistenceConfig } from "./src/config";
|
|
6
6
|
import { runDream } from "./src/dream";
|
|
7
7
|
import { runExtract } from "./src/extract";
|
|
8
8
|
import {
|
|
9
9
|
buildInjection,
|
|
10
|
-
buildSurfacingPrompt,
|
|
11
10
|
injectSurfacedContent,
|
|
12
11
|
loadIndexSnapshot,
|
|
13
12
|
runSideQuery,
|
|
@@ -18,6 +17,14 @@ import { readDreamMeta, shouldNudge, writeDreamMeta } from "./src/nudge";
|
|
|
18
17
|
import { resolveMemoryDir } from "./src/paths";
|
|
19
18
|
import { searchSessions } from "./src/session-search";
|
|
20
19
|
|
|
20
|
+
function resolveDefault(cfg: MemoryConfig, task: "dream" | "autoSurfacing" | "extractMemories", key: "model"): string | undefined;
|
|
21
|
+
function resolveDefault(cfg: MemoryConfig, task: "dream" | "autoSurfacing" | "extractMemories", key: "sessionPersistence"): SessionPersistenceConfig | undefined;
|
|
22
|
+
function resolveDefault(cfg: MemoryConfig, task: "dream" | "autoSurfacing" | "extractMemories", key: "model" | "sessionPersistence"): string | SessionPersistenceConfig | undefined {
|
|
23
|
+
const perTask = cfg[task][key];
|
|
24
|
+
if (perTask !== undefined) return perTask;
|
|
25
|
+
return cfg.defaults?.[key];
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export default function (pi: ExtensionAPI) {
|
|
22
29
|
let memoryDir: string | null = null;
|
|
23
30
|
let config: MemoryConfig | null = null;
|
|
@@ -55,7 +62,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
55
62
|
if (ok) {
|
|
56
63
|
// Fire-and-forget: does not block session_start. The headless
|
|
57
64
|
// dream agent runs independently; completion notifies the user.
|
|
58
|
-
const dreamModel = config
|
|
65
|
+
const dreamModel = resolveDefault(config, "dream", "model");
|
|
59
66
|
const dreamThinkLevel = config.dream.thinkLevel;
|
|
60
67
|
const dir = memoryDir;
|
|
61
68
|
ctx.ui.setStatus("dream", "Consolidating memory...");
|
|
@@ -65,6 +72,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
72
|
memoryDir: dir,
|
|
66
73
|
modelRegistry: ctx.modelRegistry,
|
|
67
74
|
parentModel: ctx.model,
|
|
75
|
+
sessionPersistence: resolveDefault(config, "dream", "sessionPersistence"),
|
|
68
76
|
})
|
|
69
77
|
.then(async (summary) => {
|
|
70
78
|
await writeDreamMeta(dir, sessions);
|
|
@@ -98,17 +106,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
98
106
|
if (ctx.hasUI) ctx.ui.setStatus("surfacing", "Searching relevant memories…");
|
|
99
107
|
const manifest = await scanTopics(memoryDir);
|
|
100
108
|
if (manifest.length > 0) {
|
|
101
|
-
const queryPrompt = buildSurfacingPrompt(manifest, event.prompt.slice(0, 4000), injectedTopics);
|
|
102
109
|
const selected = await runSideQuery(
|
|
103
|
-
queryPrompt,
|
|
104
110
|
manifest,
|
|
111
|
+
event.prompt.slice(0, 4000),
|
|
112
|
+
injectedTopics,
|
|
105
113
|
autoSurfacing.maxFiles,
|
|
106
114
|
autoSurfacing.thinkLevel,
|
|
107
|
-
autoSurfacing
|
|
115
|
+
resolveDefault(config, "autoSurfacing", "model"),
|
|
108
116
|
ctx.modelRegistry,
|
|
109
117
|
ctx.model,
|
|
110
118
|
memoryDir,
|
|
111
|
-
|
|
119
|
+
resolveDefault(config, "autoSurfacing", "sessionPersistence"),
|
|
112
120
|
);
|
|
113
121
|
if (selected.length > 0) {
|
|
114
122
|
const content = await injectSurfacedContent(
|
|
@@ -143,11 +151,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
151
|
if (!extractConfig?.enabled) return;
|
|
144
152
|
if (!event.messages || event.messages.length === 0) return;
|
|
145
153
|
runExtract({
|
|
146
|
-
model:
|
|
154
|
+
model: resolveDefault(config, "extractMemories", "model"),
|
|
147
155
|
thinkLevel: extractConfig.thinkLevel,
|
|
148
156
|
memoryDir,
|
|
149
157
|
modelRegistry: ctx.modelRegistry,
|
|
150
158
|
parentModel: ctx.model,
|
|
159
|
+
sessionPersistence: resolveDefault(config, "extractMemories", "sessionPersistence"),
|
|
151
160
|
messages: event.messages.map((m) => ({
|
|
152
161
|
// biome-ignore lint/suspicious/noExplicitAny: pi event message union type
|
|
153
162
|
role: String((m as any).role ?? ""),
|
|
@@ -208,11 +217,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
208
217
|
const dir = memoryDir;
|
|
209
218
|
ctx.ui.setStatus("dream", "Consolidating memory...");
|
|
210
219
|
runDream({
|
|
211
|
-
model: config
|
|
220
|
+
model: resolveDefault(config, "dream", "model"),
|
|
212
221
|
thinkLevel: config.dream.thinkLevel,
|
|
213
222
|
memoryDir,
|
|
214
223
|
modelRegistry: ctx.modelRegistry,
|
|
215
224
|
parentModel: ctx.model,
|
|
225
|
+
sessionPersistence: resolveDefault(config, "dream", "sessionPersistence"),
|
|
216
226
|
})
|
|
217
227
|
.then(async (summary) => {
|
|
218
228
|
const sessions = (await SessionManager.list(ctx.cwd)).length;
|
package/package.json
CHANGED
package/src/agent-runner.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import {
|
|
3
4
|
type AgentSession,
|
|
4
5
|
type AgentSessionEvent,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
SettingsManager,
|
|
10
11
|
} from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { MEMORY_AGENT_TOOLS } from "./agent-config";
|
|
12
|
-
import type { ThinkLevel } from "./config";
|
|
13
|
+
import type { SessionPersistenceConfig, ThinkLevel } from "./config";
|
|
13
14
|
import { resolveModel } from "./model-resolver";
|
|
14
15
|
|
|
15
16
|
export interface HeadlessAgentOpts {
|
|
@@ -22,13 +23,16 @@ export interface HeadlessAgentOpts {
|
|
|
22
23
|
maxTurns?: number;
|
|
23
24
|
signal?: AbortSignal;
|
|
24
25
|
timeoutMs?: number;
|
|
26
|
+
/** Session persistence config. When enabled, sessions are written to disk. */
|
|
27
|
+
sessionPersistence?: SessionPersistenceConfig;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const GRACE_TURNS = 1;
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
|
-
* Run a headless memory-agent sub-session: create
|
|
31
|
-
*
|
|
33
|
+
* Run a headless memory-agent sub-session: create a session (in-memory by default,
|
|
34
|
+
* persisted to disk when sessionPersistence.enabled is true), drive the turn loop,
|
|
35
|
+
* collect the assistant response text, and dispose.
|
|
32
36
|
*
|
|
33
37
|
* Does NOT call bindExtensions — no extension hooks fire in the sub-session,
|
|
34
38
|
* so pi-memory's own before_agent_start cannot recurse.
|
|
@@ -53,25 +57,31 @@ export async function runHeadlessAgent(opts: HeadlessAgentOpts): Promise<string>
|
|
|
53
57
|
});
|
|
54
58
|
await loader.reload();
|
|
55
59
|
|
|
56
|
-
// 3.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// 3. Create session (in-memory or persisted based on config)
|
|
61
|
+
const sessionManager = opts.sessionPersistence?.enabled
|
|
62
|
+
? SessionManager.create(
|
|
63
|
+
opts.cwd,
|
|
64
|
+
opts.sessionPersistence.sessionDir ?? join(opts.cwd, "sessions"),
|
|
65
|
+
)
|
|
66
|
+
: SessionManager.inMemory(opts.cwd);
|
|
62
67
|
|
|
63
|
-
// 4. Create the in-memory session (no bindExtensions)
|
|
64
68
|
const created = await createAgentSession({
|
|
65
69
|
cwd: opts.cwd,
|
|
66
70
|
tools: [...MEMORY_AGENT_TOOLS],
|
|
67
71
|
model: resolvedModel as any,
|
|
68
72
|
thinkingLevel: opts.thinkLevel as any,
|
|
69
73
|
modelRegistry: opts.modelRegistry,
|
|
70
|
-
sessionManager
|
|
74
|
+
sessionManager,
|
|
71
75
|
settingsManager,
|
|
72
76
|
resourceLoader: loader,
|
|
73
77
|
});
|
|
74
|
-
|
|
78
|
+
|
|
79
|
+
// 4. Forward abort signal after session exists (avoid listener leak if creation throws)
|
|
80
|
+
let session: AgentSession | undefined = created.session as AgentSession;
|
|
81
|
+
const onAbort = (): void => {
|
|
82
|
+
void session?.abort();
|
|
83
|
+
};
|
|
84
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
75
85
|
|
|
76
86
|
// 5. Collect response text + enforce turn limits
|
|
77
87
|
let text = "";
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,20 @@ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
|
|
6
6
|
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
7
7
|
|
|
8
|
+
/** Session persistence configuration for headless memory-agent sub-sessions. */
|
|
9
|
+
export interface SessionPersistenceConfig {
|
|
10
|
+
/** Enable disk persistence (default: false = in-memory). */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
/** Custom session directory. Defaults to memoryDir/sessions/. */
|
|
13
|
+
sessionDir?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Shared defaults that per-task configs inherit. Per-task fields override these. */
|
|
17
|
+
export interface DefaultsConfig {
|
|
18
|
+
model?: string;
|
|
19
|
+
sessionPersistence?: SessionPersistenceConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export interface AutoSurfacingConfig {
|
|
9
23
|
enabled: boolean;
|
|
10
24
|
model?: string;
|
|
@@ -23,13 +37,25 @@ export interface ExtractMemoriesConfig {
|
|
|
23
37
|
|
|
24
38
|
export interface MemoryConfig {
|
|
25
39
|
enabled: boolean;
|
|
40
|
+
/** Shared defaults for model and sessionPersistence. Per-task configs override. */
|
|
41
|
+
defaults?: DefaultsConfig;
|
|
26
42
|
memoryDir: string;
|
|
27
43
|
memIndexMaxLines: number;
|
|
28
44
|
memIndexMaxBytes: number;
|
|
29
|
-
dream: {
|
|
45
|
+
dream: {
|
|
46
|
+
nudgeAfterSessions: number;
|
|
47
|
+
nudgeAfterHours: number;
|
|
48
|
+
model?: string;
|
|
49
|
+
thinkLevel: ThinkLevel;
|
|
50
|
+
sessionPersistence?: SessionPersistenceConfig;
|
|
51
|
+
};
|
|
30
52
|
sessionSearch: { maxSessions: number; maxMatches: number };
|
|
31
|
-
autoSurfacing: AutoSurfacingConfig
|
|
32
|
-
|
|
53
|
+
autoSurfacing: AutoSurfacingConfig & {
|
|
54
|
+
sessionPersistence?: SessionPersistenceConfig;
|
|
55
|
+
};
|
|
56
|
+
extractMemories: ExtractMemoriesConfig & {
|
|
57
|
+
sessionPersistence?: SessionPersistenceConfig;
|
|
58
|
+
};
|
|
33
59
|
}
|
|
34
60
|
|
|
35
61
|
export const DEFAULT_CONFIG: MemoryConfig = {
|
package/src/dream.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { runHeadlessAgent } from "./agent-runner";
|
|
4
|
-
import type { ThinkLevel } from "./config";
|
|
4
|
+
import type { SessionPersistenceConfig, ThinkLevel } from "./config";
|
|
5
5
|
|
|
6
6
|
/** Build dream consolidation task. (unchanged) */
|
|
7
7
|
export function buildDreamTask(memoryDir: string, maxLines: number): string {
|
|
@@ -56,6 +56,7 @@ export interface RunDreamOpts {
|
|
|
56
56
|
memoryDir: string;
|
|
57
57
|
modelRegistry: ModelRegistry;
|
|
58
58
|
parentModel?: Model<any>;
|
|
59
|
+
sessionPersistence?: SessionPersistenceConfig;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/** Run dream consolidation via a headless agent. Fire-and-forget (caller decides await). */
|
|
@@ -70,5 +71,6 @@ export async function runDream(opts: RunDreamOpts): Promise<string> {
|
|
|
70
71
|
thinkLevel: opts.thinkLevel,
|
|
71
72
|
maxTurns: undefined,
|
|
72
73
|
timeoutMs: 600_000,
|
|
74
|
+
sessionPersistence: opts.sessionPersistence,
|
|
73
75
|
});
|
|
74
76
|
}
|
package/src/extract.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { runHeadlessAgent } from "./agent-runner";
|
|
4
|
-
import type { ThinkLevel } from "./config";
|
|
4
|
+
import type { SessionPersistenceConfig, ThinkLevel } from "./config";
|
|
5
5
|
|
|
6
6
|
export interface RunExtractOpts {
|
|
7
7
|
model?: string;
|
|
@@ -11,6 +11,7 @@ export interface RunExtractOpts {
|
|
|
11
11
|
maxContextTokens: number;
|
|
12
12
|
modelRegistry: ModelRegistry;
|
|
13
13
|
parentModel?: Model<any>;
|
|
14
|
+
sessionPersistence?: SessionPersistenceConfig;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/** Build extraction task prompt. (unchanged) */
|
|
@@ -90,6 +91,7 @@ export async function runExtract(opts: RunExtractOpts): Promise<void> {
|
|
|
90
91
|
thinkLevel: opts.thinkLevel,
|
|
91
92
|
maxTurns: 5,
|
|
92
93
|
timeoutMs: 120_000,
|
|
94
|
+
sessionPersistence: opts.sessionPersistence,
|
|
93
95
|
}).catch(() => {
|
|
94
96
|
/* silently ignore extract errors */
|
|
95
97
|
});
|
package/src/inject.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import type { Model } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { runHeadlessAgent } from "./agent-runner";
|
|
6
|
-
import type { ThinkLevel } from "./config";
|
|
6
|
+
import type { SessionPersistenceConfig, ThinkLevel } from "./config";
|
|
7
7
|
import { truncateForInjection } from "./index-file";
|
|
8
8
|
import { parseFrontmatter } from "./topic-file";
|
|
9
9
|
|
|
@@ -60,25 +60,15 @@ export async function scanTopics(memoryDir: string): Promise<TopicManifest[]> {
|
|
|
60
60
|
return manifests.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
manifest: TopicManifest[],
|
|
65
|
-
userPrompt: string,
|
|
66
|
-
injectedTopics: Set<string>,
|
|
67
|
-
): string {
|
|
63
|
+
function buildSurfacingPrompt(manifest: TopicManifest[], userPrompt: string): string {
|
|
68
64
|
const lines = manifest.map((t) => {
|
|
69
|
-
|
|
70
|
-
return `[${t.type}] ${t.filename} — ${t.description.slice(0, 80)}${marker}`;
|
|
65
|
+
return `[${t.type}] ${t.filename} — ${t.description.slice(0, 80)}`;
|
|
71
66
|
});
|
|
72
|
-
const alreadyInjected =
|
|
73
|
-
injectedTopics.size > 0
|
|
74
|
-
? `\n\nNote: ${injectedTopics.size} topic file(s) have already been injected in this session and are marked [already injected]. Prefer selecting uninjected topics.`
|
|
75
|
-
: "";
|
|
76
67
|
|
|
77
68
|
return [
|
|
78
69
|
"You are a memory relevance selector. Below is a list of memory topic files and a user message.",
|
|
79
70
|
"Select up to N topic files that are relevant to the user's current query.",
|
|
80
71
|
"Response format: JSON with a 'selected_files' array of filenames.",
|
|
81
|
-
alreadyInjected,
|
|
82
72
|
"",
|
|
83
73
|
"=== Topic Files ===",
|
|
84
74
|
...lines,
|
|
@@ -148,19 +138,21 @@ function parseSelectedFiles(result: string, candidates: TopicManifest[], maxFile
|
|
|
148
138
|
/** Run a lightweight headless side-query to select relevant topic files.
|
|
149
139
|
* Returns [] on timeout/failure — no fallback. */
|
|
150
140
|
export async function runSideQuery(
|
|
151
|
-
prompt: string,
|
|
152
141
|
manifest: TopicManifest[],
|
|
142
|
+
userPrompt: string,
|
|
143
|
+
injectedTopics: Set<string>,
|
|
153
144
|
maxFiles: number,
|
|
154
145
|
thinkLevel: ThinkLevel,
|
|
155
146
|
model: string | undefined,
|
|
156
147
|
modelRegistry: ModelRegistry,
|
|
157
148
|
parentModel: Model<any> | undefined,
|
|
158
149
|
memoryDir: string,
|
|
159
|
-
|
|
150
|
+
sessionPersistence?: SessionPersistenceConfig,
|
|
160
151
|
): Promise<string[]> {
|
|
161
152
|
const candidates = manifest.filter((t) => !injectedTopics.has(t.filename));
|
|
162
153
|
if (candidates.length === 0) return [];
|
|
163
|
-
const
|
|
154
|
+
const surfacingPrompt = buildSurfacingPrompt(candidates, userPrompt);
|
|
155
|
+
const task = buildSideQueryTask(surfacingPrompt, maxFiles);
|
|
164
156
|
try {
|
|
165
157
|
const result = await runHeadlessAgent({
|
|
166
158
|
task,
|
|
@@ -171,6 +163,7 @@ export async function runSideQuery(
|
|
|
171
163
|
thinkLevel,
|
|
172
164
|
maxTurns: 1,
|
|
173
165
|
timeoutMs: 30_000,
|
|
166
|
+
sessionPersistence,
|
|
174
167
|
});
|
|
175
168
|
return parseSelectedFiles(result, candidates, maxFiles);
|
|
176
169
|
} catch {
|