pi-para 0.5.2

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.
Files changed (198) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/README.md +305 -0
  3. package/SETUP.md +171 -0
  4. package/dist/capture.d.ts +34 -0
  5. package/dist/capture.d.ts.map +1 -0
  6. package/dist/capture.js +461 -0
  7. package/dist/capture.js.map +1 -0
  8. package/dist/cli.d.ts +15 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +405 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +15 -0
  13. package/dist/commands.d.ts.map +1 -0
  14. package/dist/commands.js +786 -0
  15. package/dist/commands.js.map +1 -0
  16. package/dist/context.d.ts +61 -0
  17. package/dist/context.d.ts.map +1 -0
  18. package/dist/context.js +289 -0
  19. package/dist/context.js.map +1 -0
  20. package/dist/daemon.d.ts +49 -0
  21. package/dist/daemon.d.ts.map +1 -0
  22. package/dist/daemon.js +226 -0
  23. package/dist/daemon.js.map +1 -0
  24. package/dist/frontmatter.d.ts +59 -0
  25. package/dist/frontmatter.d.ts.map +1 -0
  26. package/dist/frontmatter.js +170 -0
  27. package/dist/frontmatter.js.map +1 -0
  28. package/dist/gepa/dataset.d.ts +36 -0
  29. package/dist/gepa/dataset.d.ts.map +1 -0
  30. package/dist/gepa/dataset.js +148 -0
  31. package/dist/gepa/dataset.js.map +1 -0
  32. package/dist/gepa/evaluator.d.ts +51 -0
  33. package/dist/gepa/evaluator.d.ts.map +1 -0
  34. package/dist/gepa/evaluator.js +317 -0
  35. package/dist/gepa/evaluator.js.map +1 -0
  36. package/dist/gepa/index.d.ts +66 -0
  37. package/dist/gepa/index.d.ts.map +1 -0
  38. package/dist/gepa/index.js +240 -0
  39. package/dist/gepa/index.js.map +1 -0
  40. package/dist/gepa/llm.d.ts +35 -0
  41. package/dist/gepa/llm.d.ts.map +1 -0
  42. package/dist/gepa/llm.js +268 -0
  43. package/dist/gepa/llm.js.map +1 -0
  44. package/dist/gepa/optimized/capture-prompt.txt +65 -0
  45. package/dist/gepa/optimizer.d.ts +29 -0
  46. package/dist/gepa/optimizer.d.ts.map +1 -0
  47. package/dist/gepa/optimizer.js +249 -0
  48. package/dist/gepa/optimizer.js.map +1 -0
  49. package/dist/gepa/targets.d.ts +39 -0
  50. package/dist/gepa/targets.d.ts.map +1 -0
  51. package/dist/gepa/targets.js +326 -0
  52. package/dist/gepa/targets.js.map +1 -0
  53. package/dist/gepa/types.d.ts +168 -0
  54. package/dist/gepa/types.d.ts.map +1 -0
  55. package/dist/gepa/types.js +124 -0
  56. package/dist/gepa/types.js.map +1 -0
  57. package/dist/index.d.ts +21 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +294 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/ingest.d.ts +88 -0
  62. package/dist/ingest.d.ts.map +1 -0
  63. package/dist/ingest.js +105 -0
  64. package/dist/ingest.js.map +1 -0
  65. package/dist/link-discovery.d.ts +40 -0
  66. package/dist/link-discovery.d.ts.map +1 -0
  67. package/dist/link-discovery.js +122 -0
  68. package/dist/link-discovery.js.map +1 -0
  69. package/dist/link-utils.d.ts +30 -0
  70. package/dist/link-utils.d.ts.map +1 -0
  71. package/dist/link-utils.js +145 -0
  72. package/dist/link-utils.js.map +1 -0
  73. package/dist/lint.d.ts +39 -0
  74. package/dist/lint.d.ts.map +1 -0
  75. package/dist/lint.js +827 -0
  76. package/dist/lint.js.map +1 -0
  77. package/dist/maintainer.d.ts +24 -0
  78. package/dist/maintainer.d.ts.map +1 -0
  79. package/dist/maintainer.js +358 -0
  80. package/dist/maintainer.js.map +1 -0
  81. package/dist/processor.d.ts +14 -0
  82. package/dist/processor.d.ts.map +1 -0
  83. package/dist/processor.js +361 -0
  84. package/dist/processor.js.map +1 -0
  85. package/dist/query.d.ts +69 -0
  86. package/dist/query.d.ts.map +1 -0
  87. package/dist/query.js +107 -0
  88. package/dist/query.js.map +1 -0
  89. package/dist/raw.d.ts +57 -0
  90. package/dist/raw.d.ts.map +1 -0
  91. package/dist/raw.js +219 -0
  92. package/dist/raw.js.map +1 -0
  93. package/dist/redact.d.ts +21 -0
  94. package/dist/redact.d.ts.map +1 -0
  95. package/dist/redact.js +143 -0
  96. package/dist/redact.js.map +1 -0
  97. package/dist/scope.d.ts +54 -0
  98. package/dist/scope.d.ts.map +1 -0
  99. package/dist/scope.js +161 -0
  100. package/dist/scope.js.map +1 -0
  101. package/dist/scripts/bulk-link-fix.d.ts +9 -0
  102. package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
  103. package/dist/scripts/bulk-link-fix.js +106 -0
  104. package/dist/scripts/bulk-link-fix.js.map +1 -0
  105. package/dist/session-tools.d.ts +33 -0
  106. package/dist/session-tools.d.ts.map +1 -0
  107. package/dist/session-tools.js +211 -0
  108. package/dist/session-tools.js.map +1 -0
  109. package/dist/state.d.ts +54 -0
  110. package/dist/state.d.ts.map +1 -0
  111. package/dist/state.js +176 -0
  112. package/dist/state.js.map +1 -0
  113. package/dist/store.d.ts +56 -0
  114. package/dist/store.d.ts.map +1 -0
  115. package/dist/store.js +227 -0
  116. package/dist/store.js.map +1 -0
  117. package/dist/summarize.d.ts +56 -0
  118. package/dist/summarize.d.ts.map +1 -0
  119. package/dist/summarize.js +231 -0
  120. package/dist/summarize.js.map +1 -0
  121. package/dist/tag-registry.d.ts +56 -0
  122. package/dist/tag-registry.d.ts.map +1 -0
  123. package/dist/tag-registry.js +146 -0
  124. package/dist/tag-registry.js.map +1 -0
  125. package/dist/templates/prompts.d.ts +63 -0
  126. package/dist/templates/prompts.d.ts.map +1 -0
  127. package/dist/templates/prompts.js +260 -0
  128. package/dist/templates/prompts.js.map +1 -0
  129. package/dist/tools.d.ts +26 -0
  130. package/dist/tools.d.ts.map +1 -0
  131. package/dist/tools.js +1033 -0
  132. package/dist/tools.js.map +1 -0
  133. package/dist/watcher.d.ts +25 -0
  134. package/dist/watcher.d.ts.map +1 -0
  135. package/dist/watcher.js +84 -0
  136. package/dist/watcher.js.map +1 -0
  137. package/dist/webui/client/dist/index.html +1885 -0
  138. package/dist/webui/server.d.ts +31 -0
  139. package/dist/webui/server.d.ts.map +1 -0
  140. package/dist/webui/server.js +566 -0
  141. package/dist/webui/server.js.map +1 -0
  142. package/dist/wiki.d.ts +103 -0
  143. package/dist/wiki.d.ts.map +1 -0
  144. package/dist/wiki.js +428 -0
  145. package/dist/wiki.js.map +1 -0
  146. package/package.json +142 -0
  147. package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
  148. package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
  149. package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
  150. package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
  151. package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
  152. package/scripts/gepa/dataset.py +192 -0
  153. package/scripts/gepa/lm_providers.py +472 -0
  154. package/scripts/gepa/metric.py +279 -0
  155. package/scripts/gepa/optimize.py +367 -0
  156. package/scripts/gepa/program.py +99 -0
  157. package/scripts/gepa/pyproject.toml +19 -0
  158. package/scripts/gepa/run-all.sh +53 -0
  159. package/scripts/gepa/uv.lock +2241 -0
  160. package/scripts/gepa/wiki_reader.py +113 -0
  161. package/skills/para/SKILL.md +77 -0
  162. package/skills/setup/SKILL.md +20 -0
  163. package/src/capture.ts +563 -0
  164. package/src/cli.ts +416 -0
  165. package/src/commands.ts +878 -0
  166. package/src/context.ts +390 -0
  167. package/src/daemon.ts +288 -0
  168. package/src/frontmatter.ts +219 -0
  169. package/src/gepa/index.ts +287 -0
  170. package/src/gepa/optimized/capture-prompt.txt +65 -0
  171. package/src/index.ts +384 -0
  172. package/src/ingest.ts +177 -0
  173. package/src/link-discovery.ts +170 -0
  174. package/src/link-utils.ts +169 -0
  175. package/src/lint.ts +1018 -0
  176. package/src/maintainer.ts +404 -0
  177. package/src/processor.ts +415 -0
  178. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  179. package/src/python/minimax_lm.py +198 -0
  180. package/src/python/rlm_capture.py +215 -0
  181. package/src/query.ts +148 -0
  182. package/src/raw.ts +276 -0
  183. package/src/redact.ts +159 -0
  184. package/src/scope.ts +214 -0
  185. package/src/scripts/bulk-link-fix.ts +126 -0
  186. package/src/session-tools.ts +260 -0
  187. package/src/state.ts +271 -0
  188. package/src/store.ts +288 -0
  189. package/src/summarize.ts +273 -0
  190. package/src/tag-registry.ts +157 -0
  191. package/src/templates/index.md +17 -0
  192. package/src/templates/prompts.ts +274 -0
  193. package/src/templates/schema.md +116 -0
  194. package/src/tools.ts +1293 -0
  195. package/src/watcher.ts +101 -0
  196. package/src/webui/client/dist/index.html +1885 -0
  197. package/src/webui/server.ts +765 -0
  198. package/src/wiki.ts +550 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Session processor — uses Agent with session exploration tools
3
+ * to extract knowledge and write wiki pages.
4
+ */
5
+
6
+ import { Agent } from "@mariozechner/pi-agent-core";
7
+ import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
8
+ import type { Model } from "@mariozechner/pi-ai";
9
+ import type { QMDStore } from "qmd-engine";
10
+
11
+ import {
12
+ readPage,
13
+ writePage,
14
+ listPages,
15
+ writeIndex,
16
+ appendLog,
17
+ gitCommit,
18
+ PARA_CATEGORIES,
19
+ } from "./wiki.js";
20
+ import type { WikiPage, ParaCategory, PageRef } from "./wiki.js";
21
+ import { validateFrontmatter } from "./frontmatter.js";
22
+ import { extractWikilinks, autoLinkSlugs, syncFrontmatterLinks } from "./link-utils.js";
23
+ import { normalizeTags, normalizeScopes } from "./tag-registry.js";
24
+ import { redactSecrets } from "./redact.js";
25
+ import { reindex, searchWiki } from "./store.js";
26
+ import { appendSessionDigest } from "./raw.js";
27
+ import { Type } from "typebox";
28
+ import { StringEnum } from "@mariozechner/pi-ai";
29
+
30
+ import { loadSession, createSessionTools } from "./session-tools.js";
31
+ import type { LoadedSession } from "./session-tools.js";
32
+
33
+ // -- Types -------------------------------------------------------------------
34
+
35
+ export interface ProcessResult {
36
+ pagesCreated: string[];
37
+ pagesUpdated: string[];
38
+ skipped: boolean;
39
+ reason?: string;
40
+ }
41
+
42
+ // -- Wiki tools for the Agent ------------------------------------------------
43
+
44
+ const WikiWriteParams = Type.Object({
45
+ category: StringEnum(["projects", "areas", "resources", "archives"] as const),
46
+ slug: Type.String(),
47
+ title: Type.String(),
48
+ scope: Type.Array(Type.String()),
49
+ tags: Type.Array(Type.String()),
50
+ body: Type.String(),
51
+ });
52
+ type WikiWriteInput = { category: ParaCategory; slug: string; title: string; scope: string[]; tags: string[]; body: string };
53
+
54
+ const WikiQueryParams = Type.Object({ query: Type.String() });
55
+ const WikiReadParams = Type.Object({ path: Type.String({ description: "category/slug" }) });
56
+
57
+ function createWikiTools(wikiDir: string, store: QMDStore): AgentTool[] {
58
+
59
+ const wikiWrite: AgentTool<typeof WikiWriteParams> = {
60
+ name: "wiki_write",
61
+ label: "Wiki Write",
62
+ description: "Create or update a wiki page with PARA frontmatter.",
63
+ parameters: WikiWriteParams,
64
+ execute: async (_id, params: WikiWriteInput) => {
65
+ const now = new Date().toISOString();
66
+ const slug = params.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
67
+ const existing = await readPage(wikiDir, params.category, slug);
68
+
69
+ // Gather all existing slugs for auto-linking
70
+ const allRefs = await listPages(wikiDir);
71
+ const allSlugs = new Set(allRefs.map(r => r.slug));
72
+ allSlugs.add(slug);
73
+
74
+ // Auto-link slug mentions in body text, redact any secrets
75
+ const linkedBody = redactSecrets(autoLinkSlugs(params.body, allSlugs, slug)).text;
76
+
77
+ if (existing) {
78
+ // Update existing page (exact slug match)
79
+ const rawScope = params.scope.length > 0 ? params.scope : existing.frontmatter.scope;
80
+ const newScope = normalizeScopes(rawScope);
81
+ const mergedTags = [...new Set([...existing.frontmatter.tags, ...params.tags])];
82
+ const updated: WikiPage = {
83
+ ...existing,
84
+ body: linkedBody,
85
+ frontmatter: {
86
+ ...existing.frontmatter,
87
+ title: params.title,
88
+ scope: newScope,
89
+ tags: normalizeTags(mergedTags, newScope),
90
+ updated: now,
91
+ links: syncFrontmatterLinks(linkedBody),
92
+ },
93
+ };
94
+ await writePage(wikiDir, updated);
95
+ } else {
96
+ // Create new page
97
+ const newScope = normalizeScopes(params.scope);
98
+ const fm = validateFrontmatter({
99
+ title: params.title,
100
+ para: params.category,
101
+ scope: newScope,
102
+ tags: normalizeTags(params.tags, newScope),
103
+ sources: [],
104
+ created: now,
105
+ updated: now,
106
+ links: syncFrontmatterLinks(linkedBody),
107
+ });
108
+ await writePage(wikiDir, { category: params.category, slug, frontmatter: fm, body: linkedBody });
109
+ }
110
+
111
+ await reindex(store);
112
+ await rebuildIndex(wikiDir);
113
+ await gitCommit(wikiDir, `capture: ${params.category}/${slug}`);
114
+
115
+ return {
116
+ content: [{ type: "text", text: `Wrote ${params.category}/${slug}` }],
117
+ details: { path: `${params.category}/${slug}`, existed: !!existing },
118
+ };
119
+ },
120
+ };
121
+
122
+ const wikiQuery: AgentTool<typeof WikiQueryParams> = {
123
+ name: "wiki_query",
124
+ label: "Wiki Query",
125
+ description: "Search the wiki for existing pages (for deduplication).",
126
+ parameters: WikiQueryParams,
127
+ execute: async (_id, params: { query: string }) => {
128
+ const results = await searchWiki(store, params.query, { limit: 5 });
129
+ if (results.length === 0) {
130
+ return {
131
+ content: [{ type: "text", text: "No matching wiki pages found." }],
132
+ details: { count: 0 },
133
+ };
134
+ }
135
+ const text = results
136
+ .map((r: { page: PageRef; frontmatter: { title: string }; score: number }) =>
137
+ `${r.page.path}: ${r.frontmatter.title} (score: ${r.score.toFixed(3)})`,
138
+ )
139
+ .join("\n");
140
+ return {
141
+ content: [{ type: "text", text: `Found ${results.length} pages:\n${text}` }],
142
+ details: { count: results.length },
143
+ };
144
+ },
145
+ };
146
+
147
+ const wikiRead: AgentTool<typeof WikiReadParams> = {
148
+ name: "wiki_read",
149
+ label: "Wiki Read",
150
+ description: "Read an existing wiki page by path (category/slug).",
151
+ parameters: WikiReadParams,
152
+ execute: async (_id, params: { path: string }) => {
153
+ const parts = params.path.split("/");
154
+ if (parts.length !== 2) {
155
+ return {
156
+ content: [{ type: "text", text: `Invalid path: ${params.path}` }],
157
+ details: {},
158
+ };
159
+ }
160
+ const [cat, slug] = parts;
161
+ const page = await readPage(wikiDir, cat as ParaCategory, slug);
162
+ if (!page) {
163
+ return {
164
+ content: [{ type: "text", text: `Page not found: ${params.path}` }],
165
+ details: {},
166
+ };
167
+ }
168
+ return {
169
+ content: [{ type: "text", text: `# ${page.frontmatter.title}\n\n${page.body}` }],
170
+ details: { title: page.frontmatter.title },
171
+ };
172
+ },
173
+ };
174
+
175
+ return [wikiWrite, wikiQuery, wikiRead];
176
+ }
177
+
178
+ // -- Helpers -----------------------------------------------------------------
179
+
180
+ async function rebuildIndex(wikiDir: string): Promise<void> {
181
+ const allPages = await listPages(wikiDir);
182
+ const sections: Record<ParaCategory, string[]> = {
183
+ projects: [], areas: [], resources: [], archives: [],
184
+ };
185
+ for (const ref of allPages) {
186
+ const page = await readPage(wikiDir, ref.category, ref.slug);
187
+ const title = page?.frontmatter.title ?? ref.title;
188
+ const summary = page?.body.split("\n").find(l => l.trim() && !l.startsWith("#") && !l.startsWith("---"))?.trim() ?? "";
189
+ const desc = summary.length > 120 ? summary.slice(0, 117) + "..." : summary;
190
+ sections[ref.category].push(`- [[${ref.slug}]] \u2014 ${title}${desc ? ": " + desc : ""}`);
191
+ }
192
+ const indexLines = [
193
+ "# Wiki Index", "",
194
+ "## Projects", "", sections.projects.length > 0 ? sections.projects.join("\n") : "_No active projects._", "",
195
+ "## Areas", "", sections.areas.length > 0 ? sections.areas.join("\n") : "_No areas._", "",
196
+ "## Resources", "", sections.resources.length > 0 ? sections.resources.join("\n") : "_No resources._", "",
197
+ "## Archives", "", sections.archives.length > 0 ? sections.archives.join("\n") : "_No archived items._",
198
+ ];
199
+ await writeIndex(wikiDir, indexLines.join("\n"));
200
+ }
201
+
202
+ // -- System prompt -----------------------------------------------------------
203
+
204
+ const CAPTURE_SYSTEM_PROMPT = `You are a knowledge capture agent. You explore coding session transcripts and extract valuable knowledge into a PARA-structured wiki.
205
+
206
+ You have tools to explore the session:
207
+ - session_stats: Get an overview of the session (message count, topics)
208
+ - session_search: Search for keywords in the session
209
+ - session_slice: Read a specific character range of the session
210
+
211
+ And tools to write to the wiki:
212
+ - wiki_query: Search existing wiki pages (check for duplicates first!)
213
+ - wiki_read: Read an existing page
214
+ - wiki_write: Create or update a wiki page
215
+
216
+ PARA category rules (IMPORTANT):
217
+ - **resources/**: Use for almost everything — architecture docs, how-tos, debugging solutions, configs, patterns, reference material. This is the default.
218
+ - **areas/**: Only for ongoing responsibilities with no end date (e.g. server config, deployment procedures).
219
+ - **projects/**: ONLY for actual project goals with a defined end date. Do NOT use for documentation about a project's internals. A page like "pi-para-daemon" is a resource about pi-para, not a project.
220
+ - **archives/**: Never create pages here — pages get moved here when completed.
221
+
222
+ Scope rules:
223
+ - scope must be a project name in kebab-case (e.g. "pi-para", "qmd", "pi-mono")
224
+ - Do NOT put topic descriptions in scope (wrong: "session exploration", right: "pi-para")
225
+
226
+ Security rules:
227
+ - NEVER include API keys, tokens, passwords, or secrets in wiki pages
228
+ - Document WHERE the secret is stored (e.g. "API key in ~/.config/qmd/index.yml"), not the value itself
229
+
230
+ Your workflow:
231
+ 1. Call session_stats to understand the session
232
+ 2. For each topic, call wiki_query FIRST to find existing pages on that topic
233
+ 3. Call session_search or session_slice to read the relevant discussion
234
+ 4. If wiki_query found a matching page, call wiki_read to get its content, then wiki_write to UPDATE it (use the same slug)
235
+ 5. Only create a NEW page if wiki_query returned no relevant results
236
+
237
+ CRITICAL: ALWAYS prefer updating existing pages over creating new ones. The wiki already has many pages — search before writing. If you find a page covering the same topic (even with a different title), update it by using its exact slug.
238
+
239
+ Capture ANY of these — even small facts:
240
+ - Architecture decisions and rationale
241
+ - Debugging solutions (root cause + fix)
242
+ - Server/infrastructure details (IPs, paths, configs)
243
+ - Build and deployment procedures
244
+ - Tool configurations and setup steps
245
+ - Package names and versions
246
+ - Project conventions and coding patterns
247
+ - Operational knowledge (how to restart, deploy, rollback)
248
+
249
+ Each wiki page must use this format:
250
+ ## Topic
251
+ [What this covers]
252
+
253
+ ## Key Facts
254
+ - [Fact 1]
255
+ - [Fact 2]
256
+
257
+ ## Insights
258
+ - [Non-obvious finding]
259
+
260
+ ## Sources
261
+ - [Session file path]
262
+
263
+ When done, respond with a summary of what you captured.`;
264
+
265
+ // -- Main processor ----------------------------------------------------------
266
+
267
+ export async function processSession(
268
+ sessionPath: string,
269
+ wikiDir: string,
270
+ store: QMDStore,
271
+ scope: string,
272
+ model: Model<any>,
273
+ getApiKey: (provider: string) => Promise<string | undefined>,
274
+ ): Promise<ProcessResult> {
275
+ // 1. Load session
276
+ const session = loadSession(sessionPath);
277
+
278
+ // 2. Quick check: worth processing?
279
+ const { stats } = session;
280
+ const hasToolCalls = stats.toolCalls > 0;
281
+ if (!hasToolCalls && (stats.totalMessages < 4 || stats.totalChars < 200)) {
282
+ return { pagesCreated: [], pagesUpdated: [], skipped: true, reason: "trivial session" };
283
+ }
284
+
285
+ // 3. Create tools
286
+ const sessionTools = createSessionTools(session);
287
+ const wikiTools = createWikiTools(wikiDir, store);
288
+ const allTools = [...sessionTools, ...wikiTools];
289
+
290
+ // 4. Spin up Agent with context pruning for long-running exploration.
291
+ // After many tool calls, the accumulated context can exceed the LLM's
292
+ // context window. Prune old session_slice/session_search results — the
293
+ // Agent already extracted what it needed from them.
294
+ // Use 75% of the model's context window (chars ≈ tokens * 4), leave headroom.
295
+ const contextWindowChars = (model.contextWindow ?? 128_000) * 4;
296
+ const MAX_CONTEXT_CHARS = Math.floor(contextWindowChars * 0.75);
297
+
298
+ const agent = new Agent({
299
+ initialState: {
300
+ systemPrompt: CAPTURE_SYSTEM_PROMPT,
301
+ model,
302
+ tools: allTools,
303
+ messages: [],
304
+ },
305
+ getApiKey,
306
+ transformContext: async (messages) => {
307
+ // Estimate total chars
308
+ let total = CAPTURE_SYSTEM_PROMPT.length;
309
+ for (const m of messages) {
310
+ if ("content" in m) {
311
+ const c = m.content;
312
+ if (typeof c === "string") total += c.length;
313
+ else if (Array.isArray(c)) {
314
+ for (const b of c) {
315
+ if (typeof b === "object" && b !== null && "text" in b && typeof (b as { text: string }).text === "string") {
316
+ total += (b as { text: string }).text.length;
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ if (total <= MAX_CONTEXT_CHARS) return messages;
324
+
325
+ // Prune old tool results — truncate text blocks to 200 chars
326
+ return messages.map((m, i): AgentMessage => {
327
+ if (i >= messages.length - 10) return m;
328
+ if (m.role !== "toolResult") return m;
329
+ if (!("content" in m) || !Array.isArray(m.content)) return m;
330
+ const newContent = (m.content as Array<{ type: string; text: string }>).map(b => {
331
+ if (b.type === "text" && b.text.length > 200) {
332
+ return { type: "text" as const, text: b.text.slice(0, 200) + "\n[pruned]" };
333
+ }
334
+ return b;
335
+ });
336
+ return { ...m, content: newContent } as AgentMessage;
337
+ });
338
+ },
339
+ });
340
+
341
+ // 5. Prompt the agent
342
+ const promptText = [
343
+ `Process this session and capture all valuable knowledge into the wiki.`,
344
+ `Session file: ${sessionPath}`,
345
+ `Project scope: ${scope}`,
346
+ `Start by calling session_stats to understand the session.`,
347
+ ].join("\n");
348
+
349
+ try {
350
+ await agent.prompt(promptText);
351
+ } catch (err) {
352
+ const errMsg = err instanceof Error ? err.message : String(err);
353
+ console.error(`[processor] Agent error: ${errMsg}`);
354
+ return {
355
+ pagesCreated: [],
356
+ pagesUpdated: [],
357
+ skipped: true,
358
+ reason: `Agent error: ${errMsg}`,
359
+ };
360
+ }
361
+
362
+ // Debug: log what the agent did
363
+ const msgCount = agent.state.messages.length;
364
+ const toolCalls = agent.state.messages.filter(m => m.role === "toolResult").length;
365
+ const lastMsg = agent.state.messages[msgCount - 1];
366
+ let lastContent = "";
367
+ if (lastMsg && "content" in lastMsg) {
368
+ const c = lastMsg.content;
369
+ if (typeof c === "string") lastContent = c.slice(0, 200);
370
+ else if (Array.isArray(c)) {
371
+ const tb = c.find((b: any) => b.type === "text") as { text: string } | undefined;
372
+ if (tb) lastContent = tb.text.slice(0, 200);
373
+ }
374
+ }
375
+ console.log(`[processor] Agent finished: ${msgCount} messages, ${toolCalls} tool results. Last: ${lastContent}`);
376
+
377
+ // 6. Extract what was written from agent's messages
378
+ const pagesCreated: string[] = [];
379
+ const pagesUpdated: string[] = [];
380
+
381
+ for (const msg of agent.state.messages) {
382
+ if (msg.role !== "toolResult") continue;
383
+ const content = msg.content;
384
+ if (!Array.isArray(content)) continue;
385
+ for (const block of content) {
386
+ if (typeof block !== "object" || block === null) continue;
387
+ const b = block as unknown as Record<string, unknown>;
388
+ if (b.type === "text" && typeof b.text === "string" && b.text.startsWith("Wrote ")) {
389
+ const path = b.text.replace("Wrote ", "").trim();
390
+ // Check details to see if it was a create or update
391
+ pagesCreated.push(path);
392
+ }
393
+ }
394
+ }
395
+
396
+ if (pagesCreated.length === 0 && pagesUpdated.length === 0) {
397
+ return { pagesCreated: [], pagesUpdated: [], skipped: true, reason: "no pages written" };
398
+ }
399
+
400
+ // 7. Append session digest
401
+ try {
402
+ await appendSessionDigest(wikiDir, {
403
+ date: new Date().toISOString().split("T")[0],
404
+ project: scope,
405
+ sessionFile: sessionPath,
406
+ scope,
407
+ capturedPages: pagesCreated,
408
+ summary: `Daemon captured ${pagesCreated.length} page(s) from session`,
409
+ });
410
+ } catch {
411
+ // Non-fatal
412
+ }
413
+
414
+ return { pagesCreated, pagesUpdated, skipped: false };
415
+ }
@@ -0,0 +1,198 @@
1
+ """MiniMax LM for DSPy.
2
+
3
+ MiniMax provides an Anthropic-compatible API at a custom base URL.
4
+ This module wraps the Anthropic SDK to work with MiniMax endpoints.
5
+
6
+ MiniMax Models:
7
+ - MiniMax-M2: 196K context, 128K output, reasoning capable
8
+ - MiniMax-M2.1: 196K context, 128K output, reasoning capable
9
+
10
+ Endpoints:
11
+ - International: https://api.minimax.io/anthropic
12
+ - China: https://api.minimaxi.com/anthropic
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os
19
+ from typing import Any, Literal
20
+
21
+ import anthropic
22
+ from dspy.clients.base_lm import BaseLM
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # MiniMax API endpoints
27
+ MINIMAX_BASE_URL = "https://api.minimax.io/anthropic"
28
+ MINIMAX_CN_BASE_URL = "https://api.minimaxi.com/anthropic"
29
+
30
+
31
+ def get_minimax_api_key(china: bool = False) -> str | None:
32
+ """Get MiniMax API key from environment."""
33
+ if china:
34
+ return os.environ.get("MINIMAX_CN_API_KEY")
35
+ return os.environ.get("MINIMAX_API_KEY")
36
+
37
+
38
+ class MiniMaxLM(BaseLM):
39
+ """DSPy-compatible LM for MiniMax models via Anthropic-compatible API.
40
+
41
+ MiniMax provides an Anthropic Messages API compatible endpoint,
42
+ so we use the Anthropic SDK with a custom base URL.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ model: str = "MiniMax-M2",
48
+ api_key: str | None = None,
49
+ china: bool = False,
50
+ temperature: float | None = None,
51
+ max_tokens: int | None = None,
52
+ cache: bool = True,
53
+ num_retries: int = 3,
54
+ model_type: Literal["chat", "text"] = "chat",
55
+ **kwargs,
56
+ ):
57
+ """Initialize MiniMax LM.
58
+
59
+ Args:
60
+ model: Model ID (e.g., "MiniMax-M2", "MiniMax-M2.1")
61
+ api_key: MiniMax API key (or from MINIMAX_API_KEY env var)
62
+ china: Use China endpoint (api.minimaxi.com) instead of international
63
+ temperature: Sampling temperature
64
+ max_tokens: Maximum tokens to generate
65
+ """
66
+ super().__init__(
67
+ model=f"minimax/{model}" if not model.startswith("minimax/") else model,
68
+ model_type=model_type,
69
+ temperature=temperature or 0.0,
70
+ max_tokens=max_tokens or 8192,
71
+ cache=cache,
72
+ num_retries=num_retries,
73
+ )
74
+
75
+ self._model_id = model.replace("minimax/", "") if model.startswith("minimax/") else model
76
+ self._china = china
77
+ self._temperature = temperature or 0.0
78
+ self._max_tokens = max_tokens or 8192
79
+
80
+ # Get API key
81
+ self._api_key = api_key or get_minimax_api_key(china)
82
+ if not self._api_key:
83
+ env_var = "MINIMAX_CN_API_KEY" if china else "MINIMAX_API_KEY"
84
+ raise ValueError(f"No MiniMax API key found. Set {env_var} environment variable.")
85
+
86
+ # Create Anthropic client with MiniMax base URL
87
+ self._base_url = MINIMAX_CN_BASE_URL if china else MINIMAX_BASE_URL
88
+ self._client = anthropic.Anthropic(
89
+ api_key=self._api_key,
90
+ base_url=self._base_url,
91
+ default_headers={
92
+ "accept": "application/json",
93
+ },
94
+ )
95
+
96
+ def __getstate__(self) -> dict:
97
+ """Get state for pickling - exclude non-picklable client."""
98
+ state = self.__dict__.copy()
99
+ state.pop('_client', None)
100
+ return state
101
+
102
+ def __setstate__(self, state: dict) -> None:
103
+ """Restore state from pickle - recreate client."""
104
+ self.__dict__.update(state)
105
+ self._client = anthropic.Anthropic(
106
+ api_key=self._api_key,
107
+ base_url=self._base_url,
108
+ default_headers={"accept": "application/json"},
109
+ )
110
+
111
+ def _convert_messages(self, messages: list[dict]) -> tuple[list[dict], str | None]:
112
+ """Convert to Anthropic message format."""
113
+ converted = []
114
+ system = None
115
+
116
+ for msg in messages:
117
+ role = msg.get("role", "user")
118
+ content = msg.get("content", "")
119
+
120
+ if role == "system":
121
+ system = content
122
+ elif role == "assistant":
123
+ converted.append({"role": "assistant", "content": content})
124
+ else:
125
+ converted.append({"role": "user", "content": content})
126
+
127
+ return converted, system
128
+
129
+ def __call__(
130
+ self,
131
+ prompt: str | None = None,
132
+ messages: list[dict] | None = None,
133
+ **kwargs,
134
+ ) -> list[dict]:
135
+ """Call the MiniMax API.
136
+
137
+ Args:
138
+ prompt: Single prompt string
139
+ messages: List of messages (OpenAI format)
140
+ **kwargs: Additional parameters
141
+
142
+ Returns:
143
+ List of response dicts with 'text' key (DSPy format)
144
+ """
145
+ # Build messages
146
+ if messages is None:
147
+ messages = []
148
+ if prompt:
149
+ messages = [{"role": "user", "content": prompt}]
150
+
151
+ # Convert to Anthropic format
152
+ converted_messages, system = self._convert_messages(messages)
153
+
154
+ # Build request kwargs
155
+ request_kwargs: dict[str, Any] = {
156
+ "model": self._model_id,
157
+ "messages": converted_messages,
158
+ "max_tokens": kwargs.get("max_tokens", self._max_tokens),
159
+ "temperature": kwargs.get("temperature", self._temperature),
160
+ }
161
+
162
+ if system:
163
+ request_kwargs["system"] = system
164
+
165
+ # Call API with streaming to support large max_tokens
166
+ # The Anthropic SDK requires streaming for operations > 10 minutes
167
+ try:
168
+ text = ""
169
+ with self._client.messages.stream(**request_kwargs) as stream:
170
+ for chunk in stream.text_stream:
171
+ text += chunk
172
+ except anthropic.APIError as e:
173
+ logger.error("MiniMax API error: %s", e)
174
+ raise ValueError(f"MiniMax API error: {e}")
175
+
176
+ # Update history for BaseLM
177
+ self.history.append({
178
+ "prompt": prompt,
179
+ "messages": messages,
180
+ "kwargs": kwargs,
181
+ "response": {"text": text},
182
+ })
183
+
184
+ # Return in DSPy format
185
+ return [{"text": text}]
186
+
187
+
188
+ def create_minimax_lm(model: str = "MiniMax-M2", china: bool = False, **kwargs) -> MiniMaxLM:
189
+ """Create a MiniMax LM instance.
190
+
191
+ Args:
192
+ model: Model ID (without minimax/ prefix)
193
+ china: Use China endpoint
194
+ **kwargs: Additional parameters
195
+ """
196
+ if model.startswith("minimax/"):
197
+ model = model[8:]
198
+ return MiniMaxLM(model=model, china=china, **kwargs)