@oh-my-pi/pi-coding-agent 12.2.1 → 12.4.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 +30 -0
- package/package.json +7 -7
- package/src/config/keybindings.ts +2 -1
- package/src/config/settings-schema.ts +46 -0
- package/src/config/settings.ts +1 -0
- package/src/extensibility/slash-commands.ts +11 -0
- package/src/lsp/render.ts +1 -1
- package/src/memories/index.ts +1106 -0
- package/src/memories/storage.ts +563 -0
- package/src/modes/components/bash-execution.ts +16 -9
- package/src/modes/components/python-execution.ts +16 -7
- package/src/modes/components/tool-execution.ts +2 -1
- package/src/modes/controllers/command-controller.ts +46 -0
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/memories/consolidation.md +30 -0
- package/src/prompts/memories/read_path.md +11 -0
- package/src/prompts/memories/stage_one_input.md +7 -0
- package/src/prompts/memories/stage_one_system.md +21 -0
- package/src/sdk.ts +12 -0
- package/src/session/agent-session.ts +8 -0
- package/src/session/streaming-output.ts +1 -1
- package/src/system-prompt.ts +8 -9
- package/src/tools/bash-interactive.ts +10 -6
- package/src/tools/fetch.ts +1 -1
- package/src/tools/output-meta.ts +6 -2
- package/src/web/scrapers/types.ts +1 -0
|
@@ -17,6 +17,7 @@ import { reset as resetCapabilities } from "../../capability";
|
|
|
17
17
|
import { loadCustomShare } from "../../export/custom-share";
|
|
18
18
|
import type { CompactOptions } from "../../extensibility/extensions/types";
|
|
19
19
|
import { getGatewayStatus } from "../../ipy/gateway-coordinator";
|
|
20
|
+
import { buildMemoryToolDeveloperInstructions, clearMemoryData, enqueueMemoryConsolidation } from "../../memories";
|
|
20
21
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
21
22
|
import { BorderedLoader } from "../../modes/components/bordered-loader";
|
|
22
23
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
@@ -408,6 +409,51 @@ export class CommandController {
|
|
|
408
409
|
this.ctx.ui.requestRender();
|
|
409
410
|
}
|
|
410
411
|
|
|
412
|
+
async handleMemoryCommand(text: string): Promise<void> {
|
|
413
|
+
const argumentText = text.slice(7).trim();
|
|
414
|
+
const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
|
|
415
|
+
const agentDir = this.ctx.settings.getAgentDir();
|
|
416
|
+
|
|
417
|
+
if (action === "view") {
|
|
418
|
+
const payload = await buildMemoryToolDeveloperInstructions(agentDir, this.ctx.settings);
|
|
419
|
+
if (!payload) {
|
|
420
|
+
this.ctx.showWarning("Memory payload is empty (memories disabled or no memory summary found).");
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
424
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
425
|
+
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Memory Injection Payload")), 1, 0));
|
|
426
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
427
|
+
this.ctx.chatContainer.addChild(new Markdown(payload, 1, 1, getMarkdownTheme()));
|
|
428
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
429
|
+
this.ctx.ui.requestRender();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (action === "reset" || action === "clear") {
|
|
434
|
+
try {
|
|
435
|
+
await clearMemoryData(agentDir, this.ctx.sessionManager.getCwd());
|
|
436
|
+
await this.ctx.session.refreshBaseSystemPrompt();
|
|
437
|
+
this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
|
|
438
|
+
} catch (error) {
|
|
439
|
+
this.ctx.showError(`Memory clear failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (action === "enqueue" || action === "rebuild") {
|
|
445
|
+
try {
|
|
446
|
+
enqueueMemoryConsolidation(agentDir);
|
|
447
|
+
this.ctx.showStatus("Memory consolidation enqueued.");
|
|
448
|
+
} catch (error) {
|
|
449
|
+
this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild>");
|
|
455
|
+
}
|
|
456
|
+
|
|
411
457
|
async handleClearCommand(): Promise<void> {
|
|
412
458
|
if (this.ctx.loadingAnimation) {
|
|
413
459
|
this.ctx.loadingAnimation.stop();
|
|
@@ -333,6 +333,11 @@ export class InputController {
|
|
|
333
333
|
this.ctx.editor.setText("");
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
336
|
+
if (text === "/memory" || text.startsWith("/memory ")) {
|
|
337
|
+
this.ctx.editor.setText("");
|
|
338
|
+
await this.ctx.handleMemoryCommand(text);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
336
341
|
if (text === "/resume") {
|
|
337
342
|
this.ctx.showSessionSelector();
|
|
338
343
|
this.ctx.editor.setText("");
|
|
@@ -919,6 +919,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
919
919
|
return this.#commandController.handleMoveCommand(targetPath);
|
|
920
920
|
}
|
|
921
921
|
|
|
922
|
+
handleMemoryCommand(text: string): Promise<void> {
|
|
923
|
+
return this.#commandController.handleMemoryCommand(text);
|
|
924
|
+
}
|
|
925
|
+
|
|
922
926
|
showDebugSelector(): void {
|
|
923
927
|
this.#selectorController.showDebugSelector();
|
|
924
928
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -151,6 +151,7 @@ export interface InteractiveModeContext {
|
|
|
151
151
|
handleCompactCommand(customInstructions?: string): Promise<void>;
|
|
152
152
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
153
153
|
handleMoveCommand(targetPath: string): Promise<void>;
|
|
154
|
+
handleMemoryCommand(text: string): Promise<void>;
|
|
154
155
|
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
|
|
155
156
|
openInBrowser(urlOrPath: string): void;
|
|
156
157
|
refreshSlashCommandState(cwd?: string): Promise<void>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
You are the memory consolidation agent.
|
|
2
|
+
Memory root: {{memory_root}}
|
|
3
|
+
Input corpus (raw memories):
|
|
4
|
+
{{raw_memories}}
|
|
5
|
+
Input corpus (rollout summaries):
|
|
6
|
+
{{rollout_summaries}}
|
|
7
|
+
Produce strict JSON only with this schema:
|
|
8
|
+
{
|
|
9
|
+
"memory_md": "string",
|
|
10
|
+
"memory_summary": "string",
|
|
11
|
+
"skills": [
|
|
12
|
+
{
|
|
13
|
+
"name": "string",
|
|
14
|
+
"content": "string",
|
|
15
|
+
"scripts": [{ "path": "string", "content": "string" }],
|
|
16
|
+
"templates": [{ "path": "string", "content": "string" }],
|
|
17
|
+
"examples": [{ "path": "string", "content": "string" }]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
Requirements:
|
|
22
|
+
- memory_md: full long-term memory document, curated and readable.
|
|
23
|
+
- memory_summary: compact prompt-time memory guidance.
|
|
24
|
+
- skills: reusable procedural playbooks. Empty array allowed.
|
|
25
|
+
- Each skill.name maps to skills/<name>/.
|
|
26
|
+
- Each skill.content maps to skills/<name>/SKILL.md.
|
|
27
|
+
- scripts/templates/examples are optional. When present, each entry writes to skills/<name>/<bucket>/<path>.
|
|
28
|
+
- Only include files worth keeping long-term; omit stale assets so they are pruned.
|
|
29
|
+
- Preserve useful prior themes; remove stale or contradictory guidance.
|
|
30
|
+
- Keep memory advisory: current repository state wins.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Memory Guidance
|
|
2
|
+
Memory root: {{base_path}}
|
|
3
|
+
Operational rules:
|
|
4
|
+
1) Read `{{base_path}}/memory_summary.md` first.
|
|
5
|
+
2) If needed, inspect `{{base_path}}/MEMORY.md` and `{{base_path}}/skills/*/SKILL.md`.
|
|
6
|
+
3) Decision boundary: trust memory for heuristics/process context; trust current repo files, runtime output, and user instruction for factual state and final decisions.
|
|
7
|
+
4) Citation policy: when memory changes your plan, cite the memory artifact path you used (for example `memories/skills/<name>/SKILL.md`) and pair it with current-repo evidence before acting.
|
|
8
|
+
5) Conflict workflow: if memory disagrees with repo state or user instruction, prefer repo/user, treat memory as stale, proceed with corrected behavior, then update/regenerate memory artifacts through normal execution.
|
|
9
|
+
6) Escalate confidence only after repository verification; memory alone is never sufficient proof.
|
|
10
|
+
Memory summary:
|
|
11
|
+
{{memory_summary}}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
You are memory-stage-one extractor.
|
|
2
|
+
|
|
3
|
+
Return strict JSON only, no markdown, no commentary.
|
|
4
|
+
|
|
5
|
+
Extraction goals:
|
|
6
|
+
- Distill reusable durable knowledge from rollout history.
|
|
7
|
+
- Keep concrete technical signal (constraints, decisions, workflows, pitfalls, resolved failures).
|
|
8
|
+
- Exclude transient chatter and low-signal noise.
|
|
9
|
+
|
|
10
|
+
Output contract (required keys):
|
|
11
|
+
{
|
|
12
|
+
"rollout_summary": "string",
|
|
13
|
+
"rollout_slug": "string | null",
|
|
14
|
+
"raw_memory": "string"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
- rollout_summary: compact synopsis of what future runs should remember.
|
|
19
|
+
- rollout_slug: short lowercase slug (letters/numbers/_), or null.
|
|
20
|
+
- raw_memory: detailed durable memory blocks with enough context to reuse.
|
|
21
|
+
- If no durable signal exists, return empty strings for rollout_summary/raw_memory and null rollout_slug.
|
package/src/sdk.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
} from "./internal-urls";
|
|
47
47
|
import { disposeAllKernelSessions } from "./ipy/executor";
|
|
48
48
|
import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
|
|
49
|
+
import { buildMemoryToolDeveloperInstructions, startMemoryStartupTask } from "./memories";
|
|
49
50
|
import { AgentSession } from "./session/agent-session";
|
|
50
51
|
import { AuthStorage } from "./session/auth-storage";
|
|
51
52
|
import { convertToLlm } from "./session/messages";
|
|
@@ -914,6 +915,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
914
915
|
|
|
915
916
|
const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
|
|
916
917
|
toolContextStore.setToolNames(toolNames);
|
|
918
|
+
const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
|
|
917
919
|
const defaultPrompt = await buildSystemPromptInternal({
|
|
918
920
|
cwd,
|
|
919
921
|
skills,
|
|
@@ -923,6 +925,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
923
925
|
toolNames,
|
|
924
926
|
rules: rulebookRules,
|
|
925
927
|
skillsSettings: settings.getGroup("skills") as SkillsSettings,
|
|
928
|
+
appendSystemPrompt: memoryInstructions,
|
|
926
929
|
});
|
|
927
930
|
|
|
928
931
|
if (options.systemPrompt === undefined) {
|
|
@@ -939,6 +942,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
939
942
|
rules: rulebookRules,
|
|
940
943
|
skillsSettings: settings.getGroup("skills") as SkillsSettings,
|
|
941
944
|
customPrompt: options.systemPrompt,
|
|
945
|
+
appendSystemPrompt: memoryInstructions,
|
|
942
946
|
});
|
|
943
947
|
}
|
|
944
948
|
return options.systemPrompt(defaultPrompt);
|
|
@@ -1133,6 +1137,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1133
1137
|
}
|
|
1134
1138
|
}
|
|
1135
1139
|
|
|
1140
|
+
startMemoryStartupTask({
|
|
1141
|
+
session,
|
|
1142
|
+
settings,
|
|
1143
|
+
modelRegistry,
|
|
1144
|
+
agentDir,
|
|
1145
|
+
taskDepth,
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1136
1148
|
debugStartup("sdk:return");
|
|
1137
1149
|
return {
|
|
1138
1150
|
session,
|
|
@@ -998,6 +998,14 @@ export class AgentSession {
|
|
|
998
998
|
}
|
|
999
999
|
}
|
|
1000
1000
|
|
|
1001
|
+
/** Rebuild the base system prompt using the current active tool set. */
|
|
1002
|
+
async refreshBaseSystemPrompt(): Promise<void> {
|
|
1003
|
+
if (!this.#rebuildSystemPrompt) return;
|
|
1004
|
+
const activeToolNames = this.getActiveToolNames();
|
|
1005
|
+
this.#baseSystemPrompt = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
1006
|
+
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1001
1009
|
/**
|
|
1002
1010
|
* Replace MCP tools in the registry and activate the latest MCP tool set immediately.
|
|
1003
1011
|
* This allows /mcp add/remove/reauth to take effect without restarting the session.
|
package/src/system-prompt.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import { getSystemInfo as getNativeSystemInfo, type SystemInfo } from "@oh-my-pi/pi-natives";
|
|
6
|
-
import { $env, logger } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { $env, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { getGpuCachePath, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
8
8
|
import { $ } from "bun";
|
|
9
9
|
import { contextFileCapability } from "./capability/context-file";
|
|
@@ -346,19 +346,18 @@ async function getEnvironmentInfo(): Promise<Array<{ label: string; value: strin
|
|
|
346
346
|
export async function resolvePromptInput(input: string | undefined, description: string): Promise<string | undefined> {
|
|
347
347
|
if (!input) {
|
|
348
348
|
return undefined;
|
|
349
|
+
} else if (input.includes("\n")) {
|
|
350
|
+
return input;
|
|
349
351
|
}
|
|
350
352
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
} catch (error) {
|
|
353
|
+
try {
|
|
354
|
+
return await Bun.file(input).text();
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (!hasFsCode(error, "ENAMETOOLONG") && !isEnoent(error)) {
|
|
356
357
|
logger.warn(`Could not read ${description} file`, { path: input, error: String(error) });
|
|
357
|
-
return input;
|
|
358
358
|
}
|
|
359
|
+
return input;
|
|
359
360
|
}
|
|
360
|
-
|
|
361
|
-
return input;
|
|
362
361
|
}
|
|
363
362
|
|
|
364
363
|
export interface LoadContextFilesOptions {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentToolContext } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import { type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { type PtyRunResult, PtySession, sanitizeText } from "@oh-my-pi/pi-natives";
|
|
3
3
|
import {
|
|
4
4
|
type Component,
|
|
5
5
|
matchesKey,
|
|
@@ -23,9 +23,8 @@ export interface BashInteractiveResult extends OutputSummary {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function normalizeCaptureChunk(chunk: string): string {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
return normalized.replace(/[\x00-\x08\x0B-\x1F\x7F]/gu, "");
|
|
26
|
+
const normalized = chunk.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n");
|
|
27
|
+
return sanitizeText(normalized);
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
const XtermTerminal = xterm.Terminal;
|
|
@@ -168,7 +167,7 @@ class BashInteractiveOverlayComponent implements Component {
|
|
|
168
167
|
const visibleLines: string[] = [];
|
|
169
168
|
for (let i = 0; i < maxContentRows; i++) {
|
|
170
169
|
const line = buffer.getLine(viewportY + i)?.translateToString(true) ?? "";
|
|
171
|
-
visibleLines.push(truncateToWidth(replaceTabs(line), innerWidth));
|
|
170
|
+
visibleLines.push(truncateToWidth(replaceTabs(sanitizeText(line)), innerWidth));
|
|
172
171
|
}
|
|
173
172
|
return visibleLines;
|
|
174
173
|
}
|
|
@@ -350,7 +349,12 @@ export async function runInteractiveBashPty(
|
|
|
350
349
|
},
|
|
351
350
|
(err, chunk) => {
|
|
352
351
|
if (err || !chunk) return;
|
|
353
|
-
|
|
352
|
+
try {
|
|
353
|
+
component.appendOutput(chunk);
|
|
354
|
+
} catch {
|
|
355
|
+
const normalizedChunk = normalizeCaptureChunk(chunk);
|
|
356
|
+
component.appendOutput(normalizedChunk);
|
|
357
|
+
}
|
|
354
358
|
const normalizedChunk = normalizeCaptureChunk(chunk);
|
|
355
359
|
pendingChunks = pendingChunks.then(() => sink.push(normalizedChunk)).catch(() => {});
|
|
356
360
|
tui.requestRender();
|
package/src/tools/fetch.ts
CHANGED
|
@@ -241,7 +241,7 @@ async function tryContentNegotiation(
|
|
|
241
241
|
if (!result.ok) return null;
|
|
242
242
|
|
|
243
243
|
const mime = normalizeMime(result.contentType);
|
|
244
|
-
if (mime.includes("markdown") || mime === "text/plain") {
|
|
244
|
+
if ((mime.includes("markdown") || mime === "text/plain") && !looksLikeHtml(result.content)) {
|
|
245
245
|
return { content: result.content, type: result.contentType };
|
|
246
246
|
}
|
|
247
247
|
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface TruncationMeta {
|
|
|
21
21
|
totalBytes: number;
|
|
22
22
|
outputLines: number;
|
|
23
23
|
outputBytes: number;
|
|
24
|
+
maxBytes?: number;
|
|
24
25
|
/** Line range shown (1-indexed, inclusive) */
|
|
25
26
|
shownRange?: { start: number; end: number };
|
|
26
27
|
/** Artifact ID if full output was saved */
|
|
@@ -128,6 +129,7 @@ export class OutputMetaBuilder {
|
|
|
128
129
|
totalBytes: result.totalBytes,
|
|
129
130
|
outputLines: result.outputLines,
|
|
130
131
|
outputBytes: result.outputBytes,
|
|
132
|
+
maxBytes: result.maxBytes,
|
|
131
133
|
shownRange: { start: shownStart, end: shownEnd },
|
|
132
134
|
artifactId,
|
|
133
135
|
nextOffset: direction === "head" ? shownEnd + 1 : undefined,
|
|
@@ -212,6 +214,7 @@ export class OutputMetaBuilder {
|
|
|
212
214
|
totalBytes,
|
|
213
215
|
outputLines,
|
|
214
216
|
outputBytes,
|
|
217
|
+
maxBytes: options.maxBytes,
|
|
215
218
|
shownRange: { start: shownStart, end: shownEnd },
|
|
216
219
|
nextOffset: options.direction === "head" ? shownEnd + 1 : undefined,
|
|
217
220
|
};
|
|
@@ -319,14 +322,15 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
|
|
|
319
322
|
const range = t.shownRange;
|
|
320
323
|
let notice: string;
|
|
321
324
|
|
|
322
|
-
if (range) {
|
|
325
|
+
if (range && range.end >= range.start) {
|
|
323
326
|
notice = `Showing lines ${range.start}-${range.end} of ${t.totalLines}`;
|
|
324
327
|
} else {
|
|
325
328
|
notice = `Showing ${t.outputLines} of ${t.totalLines} lines`;
|
|
326
329
|
}
|
|
327
330
|
|
|
328
331
|
if (t.truncatedBy === "bytes") {
|
|
329
|
-
|
|
332
|
+
const maxBytes = t.maxBytes ?? t.outputBytes;
|
|
333
|
+
notice += ` (${formatSize(maxBytes)} limit)`;
|
|
330
334
|
}
|
|
331
335
|
|
|
332
336
|
if (t.nextOffset != null) {
|
|
@@ -92,6 +92,7 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
92
92
|
"User-Agent": userAgent,
|
|
93
93
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
94
94
|
"Accept-Language": "en-US,en;q=0.5",
|
|
95
|
+
"Accept-Encoding": "identity", // Cloudflare Markdown-for-Agents returns corrupted bytes when compression is negotiated
|
|
95
96
|
...headers,
|
|
96
97
|
},
|
|
97
98
|
redirect: "follow",
|