@oh-my-pi/pi-coding-agent 13.5.1 → 13.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.
- package/CHANGELOG.md +15 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +18 -0
- package/src/debug/index.ts +11 -0
- package/src/modes/controllers/command-controller.ts +20 -0
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/theme/mermaid-cache.ts +16 -57
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/prompts/tools/checkpoint.md +16 -0
- package/src/prompts/tools/render-mermaid.md +9 -0
- package/src/prompts/tools/rewind.md +13 -0
- package/src/sdk.ts +2 -0
- package/src/session/agent-session.ts +92 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/tools/checkpoint.ts +128 -0
- package/src/tools/index.ts +13 -0
- package/src/tools/render-mermaid.ts +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.5.2] - 2026-03-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `checkpoint` tool to create context checkpoints before exploratory work, allowing you to investigate with many intermediate tool calls and minimize context cost afterward
|
|
10
|
+
- Added `rewind` tool to end an active checkpoint and replace intermediate exploration messages with a concise investigation report
|
|
11
|
+
- Added `checkpoint.enabled` setting to control availability of the checkpoint and rewind tools
|
|
12
|
+
- Added `render_mermaid` tool to convert Mermaid graph source into ASCII diagram output
|
|
13
|
+
- Added `renderMermaid.enabled` setting to control availability of the render_mermaid tool
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Changed Mermaid rendering from PNG images to ASCII diagrams in theme rendering
|
|
18
|
+
- Changed `prerenderMermaid()` function to synchronously render ASCII instead of asynchronously rendering PNG
|
|
19
|
+
|
|
5
20
|
## [13.5.0] - 2026-03-01
|
|
6
21
|
|
|
7
22
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.5.
|
|
4
|
+
"version": "13.5.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.5.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.5.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.5.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.5.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.5.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.5.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.5.2",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.5.2",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.5.2",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.5.2",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.5.2",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.5.2",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -460,11 +460,29 @@ export const SETTINGS_SCHEMA = {
|
|
|
460
460
|
description: "Enable the ast_edit tool for structural AST rewrites",
|
|
461
461
|
},
|
|
462
462
|
},
|
|
463
|
+
"renderMermaid.enabled": {
|
|
464
|
+
type: "boolean",
|
|
465
|
+
default: false,
|
|
466
|
+
ui: {
|
|
467
|
+
tab: "tools",
|
|
468
|
+
label: "Enable Render Mermaid",
|
|
469
|
+
description: "Enable the render_mermaid tool for Mermaid-to-ASCII rendering",
|
|
470
|
+
},
|
|
471
|
+
},
|
|
463
472
|
"notebook.enabled": {
|
|
464
473
|
type: "boolean",
|
|
465
474
|
default: true,
|
|
466
475
|
ui: { tab: "tools", label: "Enable Notebook", description: "Enable the notebook tool for notebook editing" },
|
|
467
476
|
},
|
|
477
|
+
"checkpoint.enabled": {
|
|
478
|
+
type: "boolean",
|
|
479
|
+
default: false,
|
|
480
|
+
ui: {
|
|
481
|
+
tab: "tools",
|
|
482
|
+
label: "Enable Checkpoint/Rewind",
|
|
483
|
+
description: "Enable the checkpoint and rewind tools for context checkpointing",
|
|
484
|
+
},
|
|
485
|
+
},
|
|
468
486
|
"fetch.enabled": {
|
|
469
487
|
type: "boolean",
|
|
470
488
|
default: true,
|
package/src/debug/index.ts
CHANGED
|
@@ -27,6 +27,11 @@ const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
|
27
27
|
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
|
28
28
|
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
|
29
29
|
{ value: "system", label: "View: system info", description: "Show environment details" },
|
|
30
|
+
{
|
|
31
|
+
value: "transcript",
|
|
32
|
+
label: "Export: TUI transcript",
|
|
33
|
+
description: "Write visible TUI conversation to a temp txt",
|
|
34
|
+
},
|
|
30
35
|
{ value: "clear-cache", label: "Clear: artifact cache", description: "Remove old session artifacts" },
|
|
31
36
|
];
|
|
32
37
|
|
|
@@ -95,6 +100,9 @@ export class DebugSelectorComponent extends Container {
|
|
|
95
100
|
case "system":
|
|
96
101
|
await this.#handleViewSystemInfo();
|
|
97
102
|
break;
|
|
103
|
+
case "transcript":
|
|
104
|
+
await this.#handleTranscriptExport();
|
|
105
|
+
break;
|
|
98
106
|
case "clear-cache":
|
|
99
107
|
await this.#handleClearCache();
|
|
100
108
|
break;
|
|
@@ -323,6 +331,9 @@ export class DebugSelectorComponent extends Container {
|
|
|
323
331
|
this.ctx.ui.requestRender();
|
|
324
332
|
}
|
|
325
333
|
|
|
334
|
+
async #handleTranscriptExport(): Promise<void> {
|
|
335
|
+
await this.ctx.handleDebugTranscriptCommand();
|
|
336
|
+
}
|
|
326
337
|
async #handleOpenArtifacts(): Promise<void> {
|
|
327
338
|
const sessionFile = this.ctx.sessionManager.getSessionFile();
|
|
328
339
|
if (!sessionFile) {
|
|
@@ -28,6 +28,7 @@ import type { AuthStorage } from "../../session/auth-storage";
|
|
|
28
28
|
import { createCompactionSummaryMessage } from "../../session/messages";
|
|
29
29
|
import { outputMeta } from "../../tools/output-meta";
|
|
30
30
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
31
|
+
import { replaceTabs } from "../../tools/render-utils";
|
|
31
32
|
import { getChangelogPath, parseChangelog } from "../../utils/changelog";
|
|
32
33
|
import { openPath } from "../../utils/open";
|
|
33
34
|
|
|
@@ -70,6 +71,25 @@ export class CommandController {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
async handleDebugTranscriptCommand(): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
const width = Math.max(1, this.ctx.ui.terminal.columns);
|
|
77
|
+
const renderedLines = this.ctx.chatContainer.render(width).map(line => replaceTabs(Bun.stripANSI(line)));
|
|
78
|
+
const rendered = renderedLines.join("\n").trimEnd();
|
|
79
|
+
if (!rendered) {
|
|
80
|
+
this.ctx.showError("No messages to dump yet.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const tmpPath = path.join(os.tmpdir(), `${Snowflake.next()}-tmp.txt`);
|
|
84
|
+
await Bun.write(tmpPath, `${rendered}\n`);
|
|
85
|
+
this.ctx.showStatus(`Debug transcript written to:\n${tmpPath}`);
|
|
86
|
+
} catch (error: unknown) {
|
|
87
|
+
this.ctx.showError(
|
|
88
|
+
`Failed to write debug transcript: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
async handleShareCommand(): Promise<void> {
|
|
74
94
|
const tmpFile = path.join(os.tmpdir(), `${Snowflake.next()}.html`);
|
|
75
95
|
const cleanupTempFile = async () => {
|
|
@@ -947,6 +947,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
947
947
|
return this.#commandController.handleDumpCommand();
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
+
handleDebugTranscriptCommand(): Promise<void> {
|
|
951
|
+
return this.#commandController.handleDebugTranscriptCommand();
|
|
952
|
+
}
|
|
953
|
+
|
|
950
954
|
handleShareCommand(): Promise<void> {
|
|
951
955
|
return this.#commandController.handleShareCommand();
|
|
952
956
|
}
|
|
@@ -1,88 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
extractMermaidBlocks,
|
|
3
|
-
type MermaidImage,
|
|
4
|
-
type MermaidRenderOptions,
|
|
5
|
-
renderMermaidToPng,
|
|
6
|
-
} from "@oh-my-pi/pi-tui";
|
|
7
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { extractMermaidBlocks, logger, renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
|
|
8
2
|
|
|
9
|
-
const cache = new Map<bigint,
|
|
10
|
-
const pending = new Map<bigint, Promise<MermaidImage | null>>();
|
|
3
|
+
const cache = new Map<bigint, string>();
|
|
11
4
|
const failed = new Set<bigint>();
|
|
12
5
|
|
|
13
|
-
const defaultOptions: MermaidRenderOptions = {
|
|
14
|
-
theme: "dark",
|
|
15
|
-
backgroundColor: "transparent",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
6
|
let onRenderNeeded: (() => void) | null = null;
|
|
19
7
|
|
|
20
8
|
/**
|
|
21
|
-
* Set callback to trigger TUI re-render when mermaid
|
|
9
|
+
* Set callback to trigger TUI re-render when mermaid ASCII renders become available.
|
|
22
10
|
*/
|
|
23
11
|
export function setMermaidRenderCallback(callback: (() => void) | null): void {
|
|
24
12
|
onRenderNeeded = callback;
|
|
25
13
|
}
|
|
26
14
|
|
|
27
15
|
/**
|
|
28
|
-
* Get a pre-rendered mermaid
|
|
16
|
+
* Get a pre-rendered mermaid ASCII diagram by hash.
|
|
29
17
|
* Returns null if not cached or rendering failed.
|
|
30
18
|
*/
|
|
31
|
-
export function
|
|
19
|
+
export function getMermaidAscii(hash: bigint): string | null {
|
|
32
20
|
return cache.get(hash) ?? null;
|
|
33
21
|
}
|
|
34
22
|
|
|
35
23
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* Calls render callback when new images are cached.
|
|
24
|
+
* Render all mermaid blocks in markdown text.
|
|
25
|
+
* Caches results and calls render callback when new diagrams are available.
|
|
39
26
|
*/
|
|
40
|
-
export
|
|
41
|
-
markdown: string,
|
|
42
|
-
options: MermaidRenderOptions = defaultOptions,
|
|
43
|
-
): Promise<void> {
|
|
27
|
+
export function prerenderMermaid(markdown: string): void {
|
|
44
28
|
const blocks = extractMermaidBlocks(markdown);
|
|
45
29
|
if (blocks.length === 0) return;
|
|
46
30
|
|
|
47
|
-
|
|
31
|
+
let hasNew = false;
|
|
48
32
|
|
|
49
33
|
for (const { source, hash } of blocks) {
|
|
50
34
|
if (cache.has(hash) || failed.has(hash)) continue;
|
|
51
35
|
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
const ascii = renderMermaidAsciiSafe(source);
|
|
37
|
+
if (ascii) {
|
|
38
|
+
cache.set(hash, ascii);
|
|
39
|
+
hasNew = true;
|
|
40
|
+
} else {
|
|
41
|
+
failed.add(hash);
|
|
56
42
|
}
|
|
57
|
-
|
|
58
|
-
promises.push(
|
|
59
|
-
promise
|
|
60
|
-
.then(image => {
|
|
61
|
-
pending.delete(hash);
|
|
62
|
-
if (image) {
|
|
63
|
-
cache.set(hash, image);
|
|
64
|
-
failed.delete(hash);
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
failed.add(hash);
|
|
68
|
-
return false;
|
|
69
|
-
})
|
|
70
|
-
.catch(error => {
|
|
71
|
-
pending.delete(hash);
|
|
72
|
-
failed.add(hash);
|
|
73
|
-
logger.warn("Mermaid render failed", {
|
|
74
|
-
hash,
|
|
75
|
-
error: error instanceof Error ? error.message : String(error),
|
|
76
|
-
});
|
|
77
|
-
return false;
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
43
|
}
|
|
81
44
|
|
|
82
|
-
|
|
83
|
-
const newImages = results.some(added => added);
|
|
84
|
-
|
|
85
|
-
if (newImages && onRenderNeeded) {
|
|
45
|
+
if (hasNew && onRenderNeeded) {
|
|
86
46
|
try {
|
|
87
47
|
onRenderNeeded();
|
|
88
48
|
} catch (error) {
|
|
@@ -107,5 +67,4 @@ export function hasPendingMermaid(markdown: string): boolean {
|
|
|
107
67
|
export function clearMermaidCache(): void {
|
|
108
68
|
cache.clear();
|
|
109
69
|
failed.clear();
|
|
110
|
-
pending.clear();
|
|
111
70
|
}
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -16,7 +16,7 @@ import chalk from "chalk";
|
|
|
16
16
|
import darkThemeJson from "./dark.json" with { type: "json" };
|
|
17
17
|
import { defaultThemes } from "./defaults";
|
|
18
18
|
import lightThemeJson from "./light.json" with { type: "json" };
|
|
19
|
-
import {
|
|
19
|
+
import { getMermaidAscii } from "./mermaid-cache";
|
|
20
20
|
|
|
21
21
|
// ============================================================================
|
|
22
22
|
// Symbol Presets
|
|
@@ -2340,7 +2340,7 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
2340
2340
|
underline: (text: string) => theme.underline(text),
|
|
2341
2341
|
strikethrough: (text: string) => chalk.strikethrough(text),
|
|
2342
2342
|
symbols: getSymbolTheme(),
|
|
2343
|
-
|
|
2343
|
+
getMermaidAscii,
|
|
2344
2344
|
highlightCode: (code: string, lang?: string): string[] => {
|
|
2345
2345
|
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
|
|
2346
2346
|
try {
|
package/src/modes/types.ts
CHANGED
|
@@ -154,6 +154,7 @@ export interface InteractiveModeContext {
|
|
|
154
154
|
handleChangelogCommand(showFull?: boolean): Promise<void>;
|
|
155
155
|
handleHotkeysCommand(): void;
|
|
156
156
|
handleDumpCommand(): Promise<void>;
|
|
157
|
+
handleDebugTranscriptCommand(): Promise<void>;
|
|
157
158
|
handleClearCommand(): Promise<void>;
|
|
158
159
|
handleForkCommand(): Promise<void>;
|
|
159
160
|
handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Creates a context checkpoint before exploratory work so you can later rewind and keep only a concise report.
|
|
2
|
+
|
|
3
|
+
Use this when you need to investigate with many intermediate tool calls (read/grep/find/lsp/etc.) and want to minimize context cost afterward.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- You **MUST** call `rewind` before yielding after starting a checkpoint.
|
|
7
|
+
- You **MUST** provide a clear `goal` explaining what you are investigating.
|
|
8
|
+
- You **MUST NOT** call `checkpoint` while another checkpoint is active.
|
|
9
|
+
- Not available in subagents.
|
|
10
|
+
|
|
11
|
+
Typical flow:
|
|
12
|
+
1. `checkpoint(goal: …)`
|
|
13
|
+
2. Perform exploratory work
|
|
14
|
+
3. `rewind(report: …)` with concise findings
|
|
15
|
+
|
|
16
|
+
After rewind, intermediate checkpoint messages are removed from active context and replaced by the report.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Convert Mermaid graph source into ASCII diagram output.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- `mermaid` (required): Mermaid graph text to render.
|
|
5
|
+
- `config` (optional): JSON render configuration (spacing and layout options).
|
|
6
|
+
Behavior:
|
|
7
|
+
- Returns ASCII diagram text.
|
|
8
|
+
- Saves full ASCII output to an artifact URL (`artifact://<id>`) when artifact storage is available.
|
|
9
|
+
- Returns an error when the Mermaid input is invalid or rendering fails.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Ends an active checkpoint and rewinds context back to that checkpoint, replacing intermediate exploration with your report.
|
|
2
|
+
|
|
3
|
+
Use this immediately after investigative work started with `checkpoint`.
|
|
4
|
+
|
|
5
|
+
Requirements:
|
|
6
|
+
- `report` is **REQUIRED** and must be concise, factual, and actionable.
|
|
7
|
+
- Include key findings, decisions, and any unresolved risks.
|
|
8
|
+
- Do not include raw scratch logs unless essential.
|
|
9
|
+
- You **MUST** call this before yielding if a checkpoint is active.
|
|
10
|
+
|
|
11
|
+
Behavior:
|
|
12
|
+
- If no checkpoint is active, this tool errors.
|
|
13
|
+
- On success, the session rewinds and keeps your report as retained context.
|
package/src/sdk.ts
CHANGED
|
@@ -806,6 +806,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
806
806
|
getCompactContext: () => session.formatCompactContext(),
|
|
807
807
|
getTodoPhases: () => session.getTodoPhases(),
|
|
808
808
|
setTodoPhases: phases => session.setTodoPhases(phases),
|
|
809
|
+
getCheckpointState: () => session.getCheckpointState(),
|
|
810
|
+
setCheckpointState: state => session.setCheckpointState(state ?? undefined),
|
|
809
811
|
allocateOutputArtifact: async toolType => {
|
|
810
812
|
try {
|
|
811
813
|
return await sessionManager.allocateArtifactPath(toolType);
|
|
@@ -84,6 +84,7 @@ import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with {
|
|
|
84
84
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
85
85
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
86
86
|
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
87
|
+
import type { CheckpointState } from "../tools/checkpoint";
|
|
87
88
|
import { outputMeta } from "../tools/output-meta";
|
|
88
89
|
import { resolveToCwd } from "../tools/path-utils";
|
|
89
90
|
import type { PendingActionStore } from "../tools/pending-action";
|
|
@@ -373,6 +374,8 @@ export class AgentSession {
|
|
|
373
374
|
#promptInFlight = false;
|
|
374
375
|
#obfuscator: SecretObfuscator | undefined;
|
|
375
376
|
#pendingActionStore: PendingActionStore | undefined;
|
|
377
|
+
#checkpointState: CheckpointState | undefined = undefined;
|
|
378
|
+
#pendingRewindReport: string | undefined = undefined;
|
|
376
379
|
#promptGeneration = 0;
|
|
377
380
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
378
381
|
|
|
@@ -513,6 +516,11 @@ export class AgentSession {
|
|
|
513
516
|
if (event.type === "turn_end" && this.#ttsrManager) {
|
|
514
517
|
this.#ttsrManager.incrementMessageCount();
|
|
515
518
|
}
|
|
519
|
+
if (event.type === "turn_end" && this.#pendingRewindReport) {
|
|
520
|
+
const report = this.#pendingRewindReport;
|
|
521
|
+
this.#pendingRewindReport = undefined;
|
|
522
|
+
await this.#applyRewind(report);
|
|
523
|
+
}
|
|
516
524
|
|
|
517
525
|
// TTSR: Check for pattern matches on assistant text/thinking and tool argument deltas
|
|
518
526
|
if (event.type === "message_update" && this.#ttsrManager?.hasRules()) {
|
|
@@ -674,7 +682,7 @@ export class AgentSession {
|
|
|
674
682
|
if (event.message.role === "toolResult") {
|
|
675
683
|
const { toolName, details, isError, content } = event.message as {
|
|
676
684
|
toolName?: string;
|
|
677
|
-
details?: { path?: string; phases?: TodoPhase[] };
|
|
685
|
+
details?: { path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
|
|
678
686
|
isError?: boolean;
|
|
679
687
|
content?: Array<TextContent | ImageContent>;
|
|
680
688
|
};
|
|
@@ -704,6 +712,23 @@ export class AgentSession {
|
|
|
704
712
|
{ deliverAs: "nextTurn" },
|
|
705
713
|
);
|
|
706
714
|
}
|
|
715
|
+
if (toolName === "checkpoint" && !isError) {
|
|
716
|
+
const checkpointEntryId = this.sessionManager.getEntries().at(-1)?.id ?? null;
|
|
717
|
+
this.#checkpointState = {
|
|
718
|
+
checkpointMessageCount: this.agent.state.messages.length,
|
|
719
|
+
checkpointEntryId,
|
|
720
|
+
startedAt: details?.startedAt ?? new Date().toISOString(),
|
|
721
|
+
};
|
|
722
|
+
this.#pendingRewindReport = undefined;
|
|
723
|
+
}
|
|
724
|
+
if (toolName === "rewind" && !isError && this.#checkpointState) {
|
|
725
|
+
const detailReport = typeof details?.report === "string" ? details.report.trim() : "";
|
|
726
|
+
const textReport = content?.find(part => part.type === "text")?.text?.trim() ?? "";
|
|
727
|
+
const report = detailReport || textReport;
|
|
728
|
+
if (report.length > 0) {
|
|
729
|
+
this.#pendingRewindReport = report;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
707
732
|
}
|
|
708
733
|
}
|
|
709
734
|
|
|
@@ -723,11 +748,18 @@ export class AgentSession {
|
|
|
723
748
|
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
|
724
749
|
}
|
|
725
750
|
|
|
751
|
+
if (msg.stopReason === "aborted" && this.#checkpointState) {
|
|
752
|
+
this.#checkpointState = undefined;
|
|
753
|
+
this.#pendingRewindReport = undefined;
|
|
754
|
+
}
|
|
726
755
|
const compactionTask = this.#checkCompaction(msg);
|
|
727
756
|
this.#trackPostPromptTask(compactionTask);
|
|
728
757
|
await compactionTask;
|
|
729
758
|
// Check for incomplete todos (unless there was an error or abort)
|
|
730
759
|
if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
|
|
760
|
+
if (this.#enforceRewindBeforeYield()) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
731
763
|
await this.#checkTodoCompletion();
|
|
732
764
|
}
|
|
733
765
|
}
|
|
@@ -1678,6 +1710,17 @@ export class AgentSession {
|
|
|
1678
1710
|
this.#planReferencePath = path;
|
|
1679
1711
|
}
|
|
1680
1712
|
|
|
1713
|
+
getCheckpointState(): CheckpointState | undefined {
|
|
1714
|
+
return this.#checkpointState;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
setCheckpointState(state: CheckpointState | undefined): void {
|
|
1718
|
+
this.#checkpointState = state;
|
|
1719
|
+
if (!state) {
|
|
1720
|
+
this.#pendingRewindReport = undefined;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1681
1724
|
/**
|
|
1682
1725
|
* Inject the plan mode context message into the conversation history.
|
|
1683
1726
|
*/
|
|
@@ -3238,6 +3281,54 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3238
3281
|
}
|
|
3239
3282
|
}
|
|
3240
3283
|
}
|
|
3284
|
+
#enforceRewindBeforeYield(): boolean {
|
|
3285
|
+
if (!this.#checkpointState || this.#pendingRewindReport) {
|
|
3286
|
+
return false;
|
|
3287
|
+
}
|
|
3288
|
+
const reminder = [
|
|
3289
|
+
"<system-warning>",
|
|
3290
|
+
"You are in an active checkpoint. You MUST call rewind with your investigation findings before yielding. Do NOT yield without completing the checkpoint.",
|
|
3291
|
+
"</system-warning>",
|
|
3292
|
+
].join("\n");
|
|
3293
|
+
this.agent.appendMessage({
|
|
3294
|
+
role: "developer",
|
|
3295
|
+
content: [{ type: "text", text: reminder }],
|
|
3296
|
+
timestamp: Date.now(),
|
|
3297
|
+
});
|
|
3298
|
+
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
3299
|
+
return true;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
async #applyRewind(report: string): Promise<void> {
|
|
3303
|
+
const checkpointState = this.#checkpointState;
|
|
3304
|
+
if (!checkpointState) {
|
|
3305
|
+
return;
|
|
3306
|
+
}
|
|
3307
|
+
const safeCount = Math.max(0, Math.min(checkpointState.checkpointMessageCount, this.agent.state.messages.length));
|
|
3308
|
+
this.agent.replaceMessages(this.agent.state.messages.slice(0, safeCount));
|
|
3309
|
+
try {
|
|
3310
|
+
this.sessionManager.branchWithSummary(checkpointState.checkpointEntryId, report, {
|
|
3311
|
+
startedAt: checkpointState.startedAt,
|
|
3312
|
+
});
|
|
3313
|
+
} catch (error) {
|
|
3314
|
+
logger.warn("Rewind branch checkpoint missing, falling back to root", {
|
|
3315
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3316
|
+
});
|
|
3317
|
+
this.sessionManager.branchWithSummary(null, report, { startedAt: checkpointState.startedAt });
|
|
3318
|
+
}
|
|
3319
|
+
const details = { startedAt: checkpointState.startedAt, rewoundAt: new Date().toISOString() };
|
|
3320
|
+
this.agent.appendMessage({
|
|
3321
|
+
role: "custom",
|
|
3322
|
+
customType: "rewind-report",
|
|
3323
|
+
content: report,
|
|
3324
|
+
display: false,
|
|
3325
|
+
details,
|
|
3326
|
+
timestamp: Date.now(),
|
|
3327
|
+
});
|
|
3328
|
+
this.sessionManager.appendCustomMessageEntry("rewind-report", report, false, details);
|
|
3329
|
+
this.#checkpointState = undefined;
|
|
3330
|
+
this.#pendingRewindReport = undefined;
|
|
3331
|
+
}
|
|
3241
3332
|
/**
|
|
3242
3333
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
3243
3334
|
*/
|
|
@@ -394,7 +394,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
394
394
|
},
|
|
395
395
|
{
|
|
396
396
|
name: "debug",
|
|
397
|
-
description: "
|
|
397
|
+
description: "Open debug tools selector",
|
|
398
398
|
handle: (_command, runtime) => {
|
|
399
399
|
runtime.ctx.showDebugSelector();
|
|
400
400
|
runtime.ctx.editor.setText("");
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
+
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
4
|
+
import checkpointDescription from "../prompts/tools/checkpoint.md" with { type: "text" };
|
|
5
|
+
import rewindDescription from "../prompts/tools/rewind.md" with { type: "text" };
|
|
6
|
+
import type { ToolSession } from ".";
|
|
7
|
+
import type { OutputMeta } from "./output-meta";
|
|
8
|
+
import { ToolError } from "./tool-errors";
|
|
9
|
+
import { toolResult } from "./tool-result";
|
|
10
|
+
|
|
11
|
+
export interface CheckpointState {
|
|
12
|
+
/** Number of in-memory messages at checkpoint (AFTER checkpoint tool result is appended) */
|
|
13
|
+
checkpointMessageCount: number;
|
|
14
|
+
/** Session entry ID at checkpoint (for session tree branching) */
|
|
15
|
+
checkpointEntryId: string | null;
|
|
16
|
+
/** Timestamp */
|
|
17
|
+
startedAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const checkpointSchema = Type.Object({
|
|
21
|
+
goal: Type.String({ description: "What you are investigating and why" }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type CheckpointParams = Static<typeof checkpointSchema>;
|
|
25
|
+
|
|
26
|
+
const rewindSchema = Type.Object({
|
|
27
|
+
report: Type.String({ description: "Concise investigation findings to retain after rewind" }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
type RewindParams = Static<typeof rewindSchema>;
|
|
31
|
+
|
|
32
|
+
export interface CheckpointToolDetails {
|
|
33
|
+
goal: string;
|
|
34
|
+
startedAt: string;
|
|
35
|
+
meta?: OutputMeta;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RewindToolDetails {
|
|
39
|
+
report: string;
|
|
40
|
+
rewound: boolean;
|
|
41
|
+
meta?: OutputMeta;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isTopLevelSession(session: ToolSession): boolean {
|
|
45
|
+
const depth = session.taskDepth;
|
|
46
|
+
return depth === undefined || depth === 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class CheckpointTool implements AgentTool<typeof checkpointSchema, CheckpointToolDetails> {
|
|
50
|
+
readonly name = "checkpoint";
|
|
51
|
+
readonly label = "Checkpoint";
|
|
52
|
+
readonly description: string;
|
|
53
|
+
readonly parameters = checkpointSchema;
|
|
54
|
+
readonly strict = true;
|
|
55
|
+
|
|
56
|
+
constructor(private readonly session: ToolSession) {
|
|
57
|
+
this.description = renderPromptTemplate(checkpointDescription);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static createIf(session: ToolSession): CheckpointTool | null {
|
|
61
|
+
if (!isTopLevelSession(session)) return null;
|
|
62
|
+
return new CheckpointTool(session);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async execute(
|
|
66
|
+
_toolCallId: string,
|
|
67
|
+
params: CheckpointParams,
|
|
68
|
+
_signal?: AbortSignal,
|
|
69
|
+
_onUpdate?: AgentToolUpdateCallback<CheckpointToolDetails>,
|
|
70
|
+
_context?: AgentToolContext,
|
|
71
|
+
): Promise<AgentToolResult<CheckpointToolDetails>> {
|
|
72
|
+
if (!isTopLevelSession(this.session)) {
|
|
73
|
+
throw new ToolError("Checkpoint not available in subagents.");
|
|
74
|
+
}
|
|
75
|
+
if (this.session.getCheckpointState?.()) {
|
|
76
|
+
throw new ToolError("Checkpoint already active.");
|
|
77
|
+
}
|
|
78
|
+
const startedAt = new Date().toISOString();
|
|
79
|
+
return toolResult<CheckpointToolDetails>({ goal: params.goal, startedAt })
|
|
80
|
+
.text(
|
|
81
|
+
[
|
|
82
|
+
"Checkpoint created.",
|
|
83
|
+
`Goal: ${params.goal}`,
|
|
84
|
+
"Run your investigation, then call rewind with a concise report.",
|
|
85
|
+
].join("\n"),
|
|
86
|
+
)
|
|
87
|
+
.done();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDetails> {
|
|
92
|
+
readonly name = "rewind";
|
|
93
|
+
readonly label = "Rewind";
|
|
94
|
+
readonly description: string;
|
|
95
|
+
readonly parameters = rewindSchema;
|
|
96
|
+
readonly strict = true;
|
|
97
|
+
|
|
98
|
+
constructor(private readonly session: ToolSession) {
|
|
99
|
+
this.description = renderPromptTemplate(rewindDescription);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static createIf(session: ToolSession): RewindTool | null {
|
|
103
|
+
if (!isTopLevelSession(session)) return null;
|
|
104
|
+
return new RewindTool(session);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async execute(
|
|
108
|
+
_toolCallId: string,
|
|
109
|
+
params: RewindParams,
|
|
110
|
+
_signal?: AbortSignal,
|
|
111
|
+
_onUpdate?: AgentToolUpdateCallback<RewindToolDetails>,
|
|
112
|
+
_context?: AgentToolContext,
|
|
113
|
+
): Promise<AgentToolResult<RewindToolDetails>> {
|
|
114
|
+
if (!isTopLevelSession(this.session)) {
|
|
115
|
+
throw new ToolError("Checkpoint not available in subagents.");
|
|
116
|
+
}
|
|
117
|
+
if (!this.session.getCheckpointState?.()) {
|
|
118
|
+
throw new ToolError("No active checkpoint.");
|
|
119
|
+
}
|
|
120
|
+
const report = params.report.trim();
|
|
121
|
+
if (report.length === 0) {
|
|
122
|
+
throw new ToolError("Report cannot be empty.");
|
|
123
|
+
}
|
|
124
|
+
return toolResult<RewindToolDetails>({ report, rewound: true })
|
|
125
|
+
.text(["Rewind requested.", "Report captured for context replacement."].join("\n"))
|
|
126
|
+
.done();
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { BashTool } from "./bash";
|
|
|
22
22
|
import { BrowserTool } from "./browser";
|
|
23
23
|
import { CalculatorTool } from "./calculator";
|
|
24
24
|
import { CancelJobTool } from "./cancel-job";
|
|
25
|
+
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
25
26
|
import { ExitPlanModeTool } from "./exit-plan-mode";
|
|
26
27
|
import { FetchTool } from "./fetch";
|
|
27
28
|
import { FindTool } from "./find";
|
|
@@ -30,6 +31,7 @@ import { NotebookTool } from "./notebook";
|
|
|
30
31
|
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
31
32
|
import { PythonTool } from "./python";
|
|
32
33
|
import { ReadTool } from "./read";
|
|
34
|
+
import { RenderMermaidTool } from "./render-mermaid";
|
|
33
35
|
import { ResolveTool } from "./resolve";
|
|
34
36
|
import { reportFindingTool } from "./review";
|
|
35
37
|
import { loadSshTool } from "./ssh";
|
|
@@ -54,6 +56,7 @@ export * from "./bash";
|
|
|
54
56
|
export * from "./browser";
|
|
55
57
|
export * from "./calculator";
|
|
56
58
|
export * from "./cancel-job";
|
|
59
|
+
export * from "./checkpoint";
|
|
57
60
|
export * from "./exit-plan-mode";
|
|
58
61
|
export * from "./fetch";
|
|
59
62
|
export * from "./find";
|
|
@@ -63,6 +66,7 @@ export * from "./notebook";
|
|
|
63
66
|
export * from "./pending-action";
|
|
64
67
|
export * from "./python";
|
|
65
68
|
export * from "./read";
|
|
69
|
+
export * from "./render-mermaid";
|
|
66
70
|
export * from "./resolve";
|
|
67
71
|
export * from "./review";
|
|
68
72
|
export * from "./ssh";
|
|
@@ -143,6 +147,10 @@ export interface ToolSession {
|
|
|
143
147
|
setTodoPhases?: (phases: TodoPhase[]) => void;
|
|
144
148
|
/** Pending action store for preview/apply workflows */
|
|
145
149
|
pendingActionStore?: import("./pending-action").PendingActionStore;
|
|
150
|
+
/** Get active checkpoint state if any. */
|
|
151
|
+
getCheckpointState?: () => CheckpointState | undefined;
|
|
152
|
+
/** Set or clear active checkpoint state. */
|
|
153
|
+
setCheckpointState?: (state: CheckpointState | null) => void;
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
@@ -150,6 +158,7 @@ type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
|
150
158
|
export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
151
159
|
ast_grep: s => new AstGrepTool(s),
|
|
152
160
|
ast_edit: s => new AstEditTool(s),
|
|
161
|
+
render_mermaid: s => new RenderMermaidTool(s),
|
|
153
162
|
ask: AskTool.createIf,
|
|
154
163
|
bash: s => new BashTool(s),
|
|
155
164
|
python: s => new PythonTool(s),
|
|
@@ -162,6 +171,8 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
162
171
|
notebook: s => new NotebookTool(s),
|
|
163
172
|
read: s => new ReadTool(s),
|
|
164
173
|
browser: s => new BrowserTool(s),
|
|
174
|
+
checkpoint: CheckpointTool.createIf,
|
|
175
|
+
rewind: RewindTool.createIf,
|
|
165
176
|
task: TaskTool.create,
|
|
166
177
|
cancel_job: CancelJobTool.createIf,
|
|
167
178
|
await: AwaitTool.createIf,
|
|
@@ -281,12 +292,14 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
281
292
|
if (name === "grep") return session.settings.get("grep.enabled");
|
|
282
293
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
283
294
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
295
|
+
if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
|
|
284
296
|
if (name === "notebook") return session.settings.get("notebook.enabled");
|
|
285
297
|
if (name === "fetch") return session.settings.get("fetch.enabled");
|
|
286
298
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
287
299
|
if (name === "lsp") return session.settings.get("lsp.enabled");
|
|
288
300
|
if (name === "calc") return session.settings.get("calc.enabled");
|
|
289
301
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
302
|
+
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
290
303
|
if (name === "task") {
|
|
291
304
|
const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
292
305
|
const currentDepth = session.taskDepth ?? 0;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type MermaidAsciiRenderOptions, renderMermaidAscii } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
5
|
+
import renderMermaidDescription from "../prompts/tools/render-mermaid.md" with { type: "text" };
|
|
6
|
+
import type { ToolSession } from "./index";
|
|
7
|
+
|
|
8
|
+
const renderMermaidSchema = Type.Object({
|
|
9
|
+
mermaid: Type.String({ description: "Mermaid graph source text" }),
|
|
10
|
+
config: Type.Optional(
|
|
11
|
+
Type.Object({
|
|
12
|
+
useAscii: Type.Optional(Type.Boolean()),
|
|
13
|
+
paddingX: Type.Optional(Type.Number()),
|
|
14
|
+
paddingY: Type.Optional(Type.Number()),
|
|
15
|
+
boxBorderPadding: Type.Optional(Type.Number()),
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
type RenderMermaidParams = Static<typeof renderMermaidSchema>;
|
|
21
|
+
|
|
22
|
+
function sanitizeRenderConfig(config: MermaidAsciiRenderOptions | undefined): MermaidAsciiRenderOptions | undefined {
|
|
23
|
+
if (!config) return undefined;
|
|
24
|
+
return {
|
|
25
|
+
useAscii: config.useAscii,
|
|
26
|
+
boxBorderPadding:
|
|
27
|
+
config.boxBorderPadding === undefined ? undefined : Math.max(0, Math.floor(config.boxBorderPadding)),
|
|
28
|
+
paddingX: config.paddingX === undefined ? undefined : Math.max(0, Math.floor(config.paddingX)),
|
|
29
|
+
paddingY: config.paddingY === undefined ? undefined : Math.max(0, Math.floor(config.paddingY)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface RenderMermaidToolDetails {
|
|
33
|
+
artifactId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
|
|
37
|
+
readonly name = "render_mermaid";
|
|
38
|
+
readonly label = "RenderMermaid";
|
|
39
|
+
readonly description: string;
|
|
40
|
+
readonly parameters = renderMermaidSchema;
|
|
41
|
+
readonly strict = true;
|
|
42
|
+
|
|
43
|
+
constructor(private readonly session: ToolSession) {
|
|
44
|
+
this.description = renderPromptTemplate(renderMermaidDescription);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async execute(
|
|
48
|
+
_toolCallId: string,
|
|
49
|
+
params: RenderMermaidParams,
|
|
50
|
+
_signal?: AbortSignal,
|
|
51
|
+
_onUpdate?: AgentToolUpdateCallback<RenderMermaidToolDetails>,
|
|
52
|
+
_context?: AgentToolContext,
|
|
53
|
+
): Promise<AgentToolResult<RenderMermaidToolDetails>> {
|
|
54
|
+
const ascii = renderMermaidAscii(params.mermaid, sanitizeRenderConfig(params.config));
|
|
55
|
+
const { path: artifactPath, id: artifactId } =
|
|
56
|
+
(await this.session.allocateOutputArtifact?.("render_mermaid")) ?? {};
|
|
57
|
+
if (artifactPath) {
|
|
58
|
+
await Bun.write(artifactPath, ascii);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const artifactLine = artifactId ? `\n\nSaved artifact: artifact://${artifactId}` : "";
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `${ascii}${artifactLine}` }],
|
|
64
|
+
details: { artifactId },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|