Compare commits
4 Commits
test-permi
...
pi-unfucke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19fe3b2fe1 | ||
|
|
3ddd057d5a | ||
|
|
4ff36aaaed | ||
|
|
768ee5071d |
174
.pi/extensions/diff.ts
Normal file
174
.pi/extensions/diff.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Diff Extension
|
||||
*
|
||||
* /diff command shows modified/deleted/new files from git status and opens
|
||||
* the selected file in VS Code's diff view.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||
import { Container, Key, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
||||
|
||||
interface FileInfo {
|
||||
status: string;
|
||||
statusLabel: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("diff", {
|
||||
description: "Show git changes and open in VS Code diff view",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("No UI available", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get changed files from git status
|
||||
const result = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
||||
|
||||
if (result.code !== 0) {
|
||||
ctx.ui.notify(`git status failed: ${result.stderr}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.stdout || !result.stdout.trim()) {
|
||||
ctx.ui.notify("No changes in working tree", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse git status output
|
||||
// Format: XY filename (where XY is two-letter status, then space, then filename)
|
||||
const lines = result.stdout.split("\n");
|
||||
const files: FileInfo[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length < 4) continue; // Need at least "XY f"
|
||||
|
||||
const status = line.slice(0, 2);
|
||||
const file = line.slice(2).trimStart();
|
||||
|
||||
// Translate status codes to short labels
|
||||
let statusLabel: string;
|
||||
if (status.includes("M")) statusLabel = "M";
|
||||
else if (status.includes("A")) statusLabel = "A";
|
||||
else if (status.includes("D")) statusLabel = "D";
|
||||
else if (status.includes("?")) statusLabel = "?";
|
||||
else if (status.includes("R")) statusLabel = "R";
|
||||
else if (status.includes("C")) statusLabel = "C";
|
||||
else statusLabel = status.trim() || "~";
|
||||
|
||||
files.push({ status: statusLabel, statusLabel, file });
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
ctx.ui.notify("No changes found", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const openSelected = async (fileInfo: FileInfo): Promise<void> => {
|
||||
try {
|
||||
// Open in VS Code diff view.
|
||||
// For untracked files, git difftool won't work, so fall back to just opening the file.
|
||||
if (fileInfo.status === "?") {
|
||||
await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd });
|
||||
return;
|
||||
}
|
||||
|
||||
const diffResult = await pi.exec("git", ["difftool", "-y", "--tool=vscode", fileInfo.file], {
|
||||
cwd: ctx.cwd,
|
||||
});
|
||||
if (diffResult.code !== 0) {
|
||||
await pi.exec("code", ["-g", fileInfo.file], { cwd: ctx.cwd });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
ctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Show file picker with SelectList
|
||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
|
||||
// Top border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
// Title
|
||||
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to diff")), 0, 0));
|
||||
|
||||
// Build select items with colored status
|
||||
const items: SelectItem[] = files.map((f) => {
|
||||
let statusColor: string;
|
||||
switch (f.status) {
|
||||
case "M":
|
||||
statusColor = theme.fg("warning", f.status);
|
||||
break;
|
||||
case "A":
|
||||
statusColor = theme.fg("success", f.status);
|
||||
break;
|
||||
case "D":
|
||||
statusColor = theme.fg("error", f.status);
|
||||
break;
|
||||
case "?":
|
||||
statusColor = theme.fg("muted", f.status);
|
||||
break;
|
||||
default:
|
||||
statusColor = theme.fg("dim", f.status);
|
||||
}
|
||||
return {
|
||||
value: f,
|
||||
label: `${statusColor} ${f.file}`,
|
||||
};
|
||||
});
|
||||
|
||||
const visibleRows = Math.min(files.length, 15);
|
||||
let currentIndex = 0;
|
||||
|
||||
const selectList = new SelectList(items, visibleRows, {
|
||||
selectedPrefix: (t) => theme.fg("accent", t),
|
||||
selectedText: (t) => t, // Keep existing colors
|
||||
description: (t) => theme.fg("muted", t),
|
||||
scrollInfo: (t) => theme.fg("dim", t),
|
||||
noMatch: (t) => theme.fg("warning", t),
|
||||
});
|
||||
selectList.onSelect = (item) => {
|
||||
void openSelected(item.value as FileInfo);
|
||||
};
|
||||
selectList.onCancel = () => done();
|
||||
selectList.onSelectionChange = (item) => {
|
||||
currentIndex = items.indexOf(item);
|
||||
};
|
||||
container.addChild(selectList);
|
||||
|
||||
// Help text
|
||||
container.addChild(
|
||||
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
// Add paging with left/right
|
||||
if (matchesKey(data, Key.left)) {
|
||||
// Page up - clamp to 0
|
||||
currentIndex = Math.max(0, currentIndex - visibleRows);
|
||||
selectList.setSelectedIndex(currentIndex);
|
||||
} else if (matchesKey(data, Key.right)) {
|
||||
// Page down - clamp to last
|
||||
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
||||
selectList.setSelectedIndex(currentIndex);
|
||||
} else {
|
||||
selectList.handleInput(data);
|
||||
}
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
173
.pi/extensions/files.ts
Normal file
173
.pi/extensions/files.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Files Extension
|
||||
*
|
||||
* /files command lists all files the model has read/written/edited in the active session branch,
|
||||
* coalesced by path and sorted newest first. Selecting a file opens it in VS Code.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||
import { Container, Key, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
||||
|
||||
interface FileEntry {
|
||||
path: string;
|
||||
operations: Set<"read" | "write" | "edit">;
|
||||
lastTimestamp: number;
|
||||
}
|
||||
|
||||
type FileToolName = "read" | "write" | "edit";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("files", {
|
||||
description: "Show files read/written/edited in this session",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) {
|
||||
ctx.ui.notify("No UI available", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current branch (path from leaf to root)
|
||||
const branch = ctx.sessionManager.getBranch();
|
||||
|
||||
// First pass: collect tool calls (id -> {path, name}) from assistant messages
|
||||
const toolCalls = new Map<string, { path: string; name: FileToolName; timestamp: number }>();
|
||||
|
||||
for (const entry of branch) {
|
||||
if (entry.type !== "message") continue;
|
||||
const msg = entry.message;
|
||||
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "toolCall") {
|
||||
const name = block.name;
|
||||
if (name === "read" || name === "write" || name === "edit") {
|
||||
const path = block.arguments?.path;
|
||||
if (path && typeof path === "string") {
|
||||
toolCalls.set(block.id, { path, name, timestamp: msg.timestamp });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: match tool results to get the actual execution timestamp
|
||||
const fileMap = new Map<string, FileEntry>();
|
||||
|
||||
for (const entry of branch) {
|
||||
if (entry.type !== "message") continue;
|
||||
const msg = entry.message;
|
||||
|
||||
if (msg.role === "toolResult") {
|
||||
const toolCall = toolCalls.get(msg.toolCallId);
|
||||
if (!toolCall) continue;
|
||||
|
||||
const { path, name } = toolCall;
|
||||
const timestamp = msg.timestamp;
|
||||
|
||||
const existing = fileMap.get(path);
|
||||
if (existing) {
|
||||
existing.operations.add(name);
|
||||
if (timestamp > existing.lastTimestamp) {
|
||||
existing.lastTimestamp = timestamp;
|
||||
}
|
||||
} else {
|
||||
fileMap.set(path, {
|
||||
path,
|
||||
operations: new Set([name]),
|
||||
lastTimestamp: timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileMap.size === 0) {
|
||||
ctx.ui.notify("No files read/written/edited in this session", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by most recent first
|
||||
const files = Array.from(fileMap.values()).sort((a, b) => b.lastTimestamp - a.lastTimestamp);
|
||||
|
||||
const openSelected = async (file: FileEntry): Promise<void> => {
|
||||
try {
|
||||
await pi.exec("code", ["-g", file.path], { cwd: ctx.cwd });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
ctx.ui.notify(`Failed to open ${file.path}: ${message}`, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Show file picker with SelectList
|
||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
|
||||
// Top border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
// Title
|
||||
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to open")), 0, 0));
|
||||
|
||||
// Build select items with colored operations
|
||||
const items: SelectItem[] = files.map((f) => {
|
||||
const ops: string[] = [];
|
||||
if (f.operations.has("read")) ops.push(theme.fg("muted", "R"));
|
||||
if (f.operations.has("write")) ops.push(theme.fg("success", "W"));
|
||||
if (f.operations.has("edit")) ops.push(theme.fg("warning", "E"));
|
||||
const opsLabel = ops.join("");
|
||||
return {
|
||||
value: f,
|
||||
label: `${opsLabel} ${f.path}`,
|
||||
};
|
||||
});
|
||||
|
||||
const visibleRows = Math.min(files.length, 15);
|
||||
let currentIndex = 0;
|
||||
|
||||
const selectList = new SelectList(items, visibleRows, {
|
||||
selectedPrefix: (t) => theme.fg("accent", t),
|
||||
selectedText: (t) => t, // Keep existing colors
|
||||
description: (t) => theme.fg("muted", t),
|
||||
scrollInfo: (t) => theme.fg("dim", t),
|
||||
noMatch: (t) => theme.fg("warning", t),
|
||||
});
|
||||
selectList.onSelect = (item) => {
|
||||
void openSelected(item.value as FileEntry);
|
||||
};
|
||||
selectList.onCancel = () => done();
|
||||
selectList.onSelectionChange = (item) => {
|
||||
currentIndex = items.indexOf(item);
|
||||
};
|
||||
container.addChild(selectList);
|
||||
|
||||
// Help text
|
||||
container.addChild(
|
||||
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
// Add paging with left/right
|
||||
if (matchesKey(data, Key.left)) {
|
||||
// Page up - clamp to 0
|
||||
currentIndex = Math.max(0, currentIndex - visibleRows);
|
||||
selectList.setSelectedIndex(currentIndex);
|
||||
} else if (matchesKey(data, Key.right)) {
|
||||
// Page down - clamp to last
|
||||
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
||||
selectList.setSelectedIndex(currentIndex);
|
||||
} else {
|
||||
selectList.handleInput(data);
|
||||
}
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
158
.pi/extensions/prompt-url-widget.ts
Normal file
158
.pi/extensions/prompt-url-widget.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { DynamicBorder, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||
import { Container, Text } from "@mariozechner/pi-tui";
|
||||
|
||||
const PR_PROMPT_PATTERN = /^\s*You are given one or more GitHub PR URLs:\s*(\S+)/im;
|
||||
const ISSUE_PROMPT_PATTERN = /^\s*Analyze GitHub issue\(s\):\s*(\S+)/im;
|
||||
|
||||
type PromptMatch = {
|
||||
kind: "pr" | "issue";
|
||||
url: string;
|
||||
};
|
||||
|
||||
type GhMetadata = {
|
||||
title?: string;
|
||||
author?: {
|
||||
login?: string;
|
||||
name?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
function extractPromptMatch(prompt: string): PromptMatch | undefined {
|
||||
const prMatch = prompt.match(PR_PROMPT_PATTERN);
|
||||
if (prMatch?.[1]) {
|
||||
return { kind: "pr", url: prMatch[1].trim() };
|
||||
}
|
||||
|
||||
const issueMatch = prompt.match(ISSUE_PROMPT_PATTERN);
|
||||
if (issueMatch?.[1]) {
|
||||
return { kind: "issue", url: issueMatch[1].trim() };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function fetchGhMetadata(
|
||||
pi: ExtensionAPI,
|
||||
kind: PromptMatch["kind"],
|
||||
url: string,
|
||||
): Promise<GhMetadata | undefined> {
|
||||
const args =
|
||||
kind === "pr" ? ["pr", "view", url, "--json", "title,author"] : ["issue", "view", url, "--json", "title,author"];
|
||||
|
||||
try {
|
||||
const result = await pi.exec("gh", args);
|
||||
if (result.code !== 0 || !result.stdout) return undefined;
|
||||
return JSON.parse(result.stdout) as GhMetadata;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function formatAuthor(author?: GhMetadata["author"]): string | undefined {
|
||||
if (!author) return undefined;
|
||||
const name = author.name?.trim();
|
||||
const login = author.login?.trim();
|
||||
if (name && login) return `${name} (@${login})`;
|
||||
if (login) return `@${login}`;
|
||||
if (name) return name;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
|
||||
const setWidget = (ctx: ExtensionContext, match: PromptMatch, title?: string, authorText?: string) => {
|
||||
ctx.ui.setWidget("prompt-url", (_tui, thm) => {
|
||||
const titleText = title ? thm.fg("accent", title) : thm.fg("accent", match.url);
|
||||
const authorLine = authorText ? thm.fg("muted", authorText) : undefined;
|
||||
const urlLine = thm.fg("dim", match.url);
|
||||
|
||||
const lines = [titleText];
|
||||
if (authorLine) lines.push(authorLine);
|
||||
lines.push(urlLine);
|
||||
|
||||
const container = new Container();
|
||||
container.addChild(new DynamicBorder((s: string) => thm.fg("muted", s)));
|
||||
container.addChild(new Text(lines.join("\n"), 1, 0));
|
||||
return container;
|
||||
});
|
||||
};
|
||||
|
||||
const applySessionName = (ctx: ExtensionContext, match: PromptMatch, title?: string) => {
|
||||
const label = match.kind === "pr" ? "PR" : "Issue";
|
||||
const trimmedTitle = title?.trim();
|
||||
const fallbackName = `${label}: ${match.url}`;
|
||||
const desiredName = trimmedTitle ? `${label}: ${trimmedTitle} (${match.url})` : fallbackName;
|
||||
const currentName = pi.getSessionName()?.trim();
|
||||
if (!currentName) {
|
||||
pi.setSessionName(desiredName);
|
||||
return;
|
||||
}
|
||||
if (currentName === match.url || currentName === fallbackName) {
|
||||
pi.setSessionName(desiredName);
|
||||
}
|
||||
};
|
||||
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
const match = extractPromptMatch(event.prompt);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWidget(ctx, match);
|
||||
applySessionName(ctx, match);
|
||||
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
|
||||
const title = meta?.title?.trim();
|
||||
const authorText = formatAuthor(meta?.author);
|
||||
setWidget(ctx, match, title, authorText);
|
||||
applySessionName(ctx, match, title);
|
||||
});
|
||||
});
|
||||
|
||||
pi.on("session_switch", async (_event, ctx) => {
|
||||
rebuildFromSession(ctx);
|
||||
});
|
||||
|
||||
const getUserText = (content: string | { type: string; text?: string }[] | undefined): string => {
|
||||
if (!content) return "";
|
||||
if (typeof content === "string") return content;
|
||||
return (
|
||||
content
|
||||
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
||||
.map((block) => block.text)
|
||||
.join("\n") ?? ""
|
||||
);
|
||||
};
|
||||
|
||||
const rebuildFromSession = (ctx: ExtensionContext) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const lastMatch = [...entries].reverse().find((entry) => {
|
||||
if (entry.type !== "message" || entry.message.role !== "user") return false;
|
||||
const text = getUserText(entry.message.content);
|
||||
return !!extractPromptMatch(text);
|
||||
});
|
||||
|
||||
const content =
|
||||
lastMatch?.type === "message" && lastMatch.message.role === "user" ? lastMatch.message.content : undefined;
|
||||
const text = getUserText(content);
|
||||
const match = text ? extractPromptMatch(text) : undefined;
|
||||
if (!match) {
|
||||
ctx.ui.setWidget("prompt-url", undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
setWidget(ctx, match);
|
||||
applySessionName(ctx, match);
|
||||
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
|
||||
const title = meta?.title?.trim();
|
||||
const authorText = formatAuthor(meta?.author);
|
||||
setWidget(ctx, match, title, authorText);
|
||||
applySessionName(ctx, match, title);
|
||||
});
|
||||
};
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
rebuildFromSession(ctx);
|
||||
});
|
||||
}
|
||||
24
.pi/extensions/redraws.ts
Normal file
24
.pi/extensions/redraws.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Redraws Extension
|
||||
*
|
||||
* Exposes /tui to show TUI redraw stats.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("tui", {
|
||||
description: "Show TUI stats",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
let redraws = 0;
|
||||
await ctx.ui.custom<void>((tui, _theme, _keybindings, done) => {
|
||||
redraws = tui.fullRedraws;
|
||||
done(undefined);
|
||||
return new Text("", 0, 0);
|
||||
});
|
||||
ctx.ui.notify(`TUI full redraws: ${redraws}`, "info");
|
||||
},
|
||||
});
|
||||
}
|
||||
2
.pi/git/.gitignore
vendored
Normal file
2
.pi/git/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
53
.pi/prompts/cl.md
Normal file
53
.pi/prompts/cl.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
description: Audit changelog entries before release
|
||||
---
|
||||
Audit changelog entries for all commits since the last release.
|
||||
|
||||
## Process
|
||||
|
||||
1. **Find the last release tag:**
|
||||
```bash
|
||||
git tag --sort=-version:refname | head -1
|
||||
```
|
||||
|
||||
2. **List all commits since that tag:**
|
||||
```bash
|
||||
git log <tag>..HEAD --oneline
|
||||
```
|
||||
|
||||
3. **Read each package's [Unreleased] section:**
|
||||
- packages/ai/CHANGELOG.md
|
||||
- packages/tui/CHANGELOG.md
|
||||
- packages/coding-agent/CHANGELOG.md
|
||||
|
||||
4. **For each commit, check:**
|
||||
- Skip: changelog updates, doc-only changes, release housekeeping
|
||||
- Determine which package(s) the commit affects (use `git show <hash> --stat`)
|
||||
- Verify a changelog entry exists in the affected package(s)
|
||||
- For external contributions (PRs), verify format: `Description ([#N](url) by [@user](url))`
|
||||
|
||||
5. **Cross-package duplication rule:**
|
||||
Changes in `ai`, `agent` or `tui` that affect end users should be duplicated to `coding-agent` changelog, since coding-agent is the user-facing package that depends on them.
|
||||
|
||||
6. **Add New Features section after changelog fixes:**
|
||||
- Insert a `### New Features` section at the start of `## [Unreleased]` in `packages/coding-agent/CHANGELOG.md`.
|
||||
- Propose the top new features to the user for confirmation before writing them.
|
||||
- Link to relevant docs and sections whenever possible.
|
||||
|
||||
7. **Report:**
|
||||
- List commits with missing entries
|
||||
- List entries that need cross-package duplication
|
||||
- Add any missing entries directly
|
||||
|
||||
## Changelog Format Reference
|
||||
|
||||
Sections (in order):
|
||||
- `### Breaking Changes` - API changes requiring migration
|
||||
- `### Added` - New features
|
||||
- `### Changed` - Changes to existing functionality
|
||||
- `### Fixed` - Bug fixes
|
||||
- `### Removed` - Removed features
|
||||
|
||||
Attribution:
|
||||
- Internal: `Fixed foo ([#123](https://github.com/badlogic/pi-mono/issues/123))`
|
||||
- External: `Added bar ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@user](https://github.com/user))`
|
||||
21
.pi/prompts/is.md
Normal file
21
.pi/prompts/is.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: Analyze GitHub issues (bugs or feature requests)
|
||||
---
|
||||
Analyze GitHub issue(s): $ARGUMENTS
|
||||
|
||||
For each issue:
|
||||
|
||||
1. Read the issue in full, including all comments and linked issues/PRs.
|
||||
|
||||
2. **For bugs**:
|
||||
- Ignore any root cause analysis in the issue (likely wrong)
|
||||
- Read all related code files in full (no truncation)
|
||||
- Trace the code path and identify the actual root cause
|
||||
- Propose a fix
|
||||
|
||||
3. **For feature requests**:
|
||||
- Read all related code files in full (no truncation)
|
||||
- Propose the most concise implementation approach
|
||||
- List affected files and changes needed
|
||||
|
||||
Do NOT implement unless explicitly asked. Analyze and propose only.
|
||||
33
.pi/prompts/pr.md
Normal file
33
.pi/prompts/pr.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
description: Review PRs from URLs with structured issue and code analysis
|
||||
---
|
||||
You are given one or more GitHub PR URLs: $@
|
||||
|
||||
For each PR URL, do the following in order:
|
||||
1. Read the PR page in full. Include description, all comments, all commits, and all changed files.
|
||||
2. Identify any linked issues referenced in the PR body, comments, commit messages, or cross links. Read each issue in full, including all comments.
|
||||
3. Analyze the PR diff. Read all relevant code files in full with no truncation from the current main branch and compare against the diff. Do not fetch PR file blobs unless a file is missing on main or the diff context is insufficient. Include related code paths that are not in the diff but are required to validate behavior.
|
||||
4. Check if docs/*.md require modification. This is usually the case when existing features have been changed, or new features have been added.
|
||||
5. Provide a structured review with these sections:
|
||||
- Good: solid choices or improvements
|
||||
- Bad: concrete issues, regressions, missing tests, or risks
|
||||
- Ugly: subtle or high impact problems
|
||||
6. Add Questions or Assumptions if anything is unclear.
|
||||
7. Add Change summary and Tests.
|
||||
|
||||
Output format per PR:
|
||||
PR: <url>
|
||||
Good:
|
||||
- ...
|
||||
Bad:
|
||||
- ...
|
||||
Ugly:
|
||||
- ...
|
||||
Questions or Assumptions:
|
||||
- ...
|
||||
Change summary:
|
||||
- ...
|
||||
Tests:
|
||||
- ...
|
||||
|
||||
If no issues are found, say so under Bad and Ugly.
|
||||
66
docs/pi-dev.md
Normal file
66
docs/pi-dev.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Pi Development Workflow
|
||||
|
||||
This guide summarizes a sane workflow for working on the pi integration in OpenClaw.
|
||||
|
||||
## Type Checking and Linting
|
||||
|
||||
- Type check and build: `pnpm build`
|
||||
- Lint: `pnpm lint`
|
||||
- Format check: `pnpm format`
|
||||
- Full gate before pushing: `pnpm lint && pnpm build && pnpm test`
|
||||
|
||||
## Running Pi Tests
|
||||
|
||||
Use the dedicated script for the pi integration test set:
|
||||
|
||||
```bash
|
||||
scripts/pi/run-tests.sh
|
||||
```
|
||||
|
||||
To include the live test that exercises real provider behavior:
|
||||
|
||||
```bash
|
||||
scripts/pi/run-tests.sh --live
|
||||
```
|
||||
|
||||
The script runs all pi related unit tests via these globs:
|
||||
|
||||
- `src/agents/pi-*.test.ts`
|
||||
- `src/agents/pi-embedded-*.test.ts`
|
||||
- `src/agents/pi-tools*.test.ts`
|
||||
- `src/agents/pi-settings.test.ts`
|
||||
- `src/agents/pi-tool-definition-adapter.test.ts`
|
||||
- `src/agents/pi-extensions/*.test.ts`
|
||||
|
||||
## Manual Testing
|
||||
|
||||
Recommended flow:
|
||||
|
||||
- Run the gateway in dev mode:
|
||||
- `pnpm gateway:dev`
|
||||
- Trigger the agent directly:
|
||||
- `pnpm openclaw agent --message "Hello" --thinking low`
|
||||
- Use the TUI for interactive debugging:
|
||||
- `pnpm tui`
|
||||
|
||||
For tool call behavior, prompt for a `read` or `exec` action so you can see tool streaming and payload handling.
|
||||
|
||||
## Clean Slate Reset
|
||||
|
||||
State lives under the OpenClaw state directory. Default is `~/.openclaw`. If `OPENCLAW_STATE_DIR` is set, use that directory instead.
|
||||
|
||||
To reset everything:
|
||||
|
||||
- `openclaw.json` for config
|
||||
- `credentials/` for auth profiles and tokens
|
||||
- `agents/<agentId>/sessions/` for agent session history
|
||||
- `agents/<agentId>/sessions.json` for the session index
|
||||
- `sessions/` if legacy paths exist
|
||||
- `workspace/` if you want a blank workspace
|
||||
|
||||
If you only want to reset sessions, delete `agents/<agentId>/sessions/` and `agents/<agentId>/sessions.json` for that agent. Keep `credentials/` if you do not want to reauthenticate.
|
||||
|
||||
## References
|
||||
|
||||
- https://docs.openclaw.ai/testing
|
||||
- https://docs.openclaw.ai/start/getting-started
|
||||
587
docs/pi.md
Normal file
587
docs/pi.md
Normal file
@@ -0,0 +1,587 @@
|
||||
# Pi Integration Architecture
|
||||
|
||||
This document describes how OpenClaw integrates with [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) and its sibling packages (`pi-ai`, `pi-agent-core`, `pi-tui`) to power its AI agent capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
OpenClaw uses the pi SDK to embed an AI coding agent into its messaging gateway architecture. Instead of spawning pi as a subprocess or using RPC mode, OpenClaw directly imports and instantiates pi's `AgentSession` via `createAgentSession()`. This embedded approach provides:
|
||||
|
||||
- Full control over session lifecycle and event handling
|
||||
- Custom tool injection (messaging, sandbox, channel-specific actions)
|
||||
- System prompt customization per channel/context
|
||||
- Session persistence with branching/compaction support
|
||||
- Multi-account auth profile rotation with failover
|
||||
- Provider-agnostic model switching
|
||||
|
||||
## Package Dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"@mariozechner/pi-agent-core": "0.49.3",
|
||||
"@mariozechner/pi-ai": "0.49.3",
|
||||
"@mariozechner/pi-coding-agent": "0.49.3",
|
||||
"@mariozechner/pi-tui": "0.49.3"
|
||||
}
|
||||
```
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `pi-ai` | Core LLM abstractions: `Model`, `streamSimple`, message types, provider APIs |
|
||||
| `pi-agent-core` | Agent loop, tool execution, `AgentMessage` types |
|
||||
| `pi-coding-agent` | High-level SDK: `createAgentSession`, `SessionManager`, `AuthStorage`, `ModelRegistry`, built-in tools |
|
||||
| `pi-tui` | Terminal UI components (used in OpenClaw's local TUI mode) |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/agents/
|
||||
├── pi-embedded-runner.ts # Re-exports from pi-embedded-runner/
|
||||
├── pi-embedded-runner/
|
||||
│ ├── run.ts # Main entry: runEmbeddedPiAgent()
|
||||
│ ├── run/
|
||||
│ │ ├── attempt.ts # Single attempt logic with session setup
|
||||
│ │ ├── params.ts # RunEmbeddedPiAgentParams type
|
||||
│ │ ├── payloads.ts # Build response payloads from run results
|
||||
│ │ ├── images.ts # Vision model image injection
|
||||
│ │ └── types.ts # EmbeddedRunAttemptResult
|
||||
│ ├── abort.ts # Abort error detection
|
||||
│ ├── cache-ttl.ts # Cache TTL tracking for context pruning
|
||||
│ ├── compact.ts # Manual/auto compaction logic
|
||||
│ ├── extensions.ts # Load pi extensions for embedded runs
|
||||
│ ├── extra-params.ts # Provider-specific stream params
|
||||
│ ├── google.ts # Google/Gemini turn ordering fixes
|
||||
│ ├── history.ts # History limiting (DM vs group)
|
||||
│ ├── lanes.ts # Session/global command lanes
|
||||
│ ├── logger.ts # Subsystem logger
|
||||
│ ├── model.ts # Model resolution via ModelRegistry
|
||||
│ ├── runs.ts # Active run tracking, abort, queue
|
||||
│ ├── sandbox-info.ts # Sandbox info for system prompt
|
||||
│ ├── session-manager-cache.ts # SessionManager instance caching
|
||||
│ ├── session-manager-init.ts # Session file initialization
|
||||
│ ├── system-prompt.ts # System prompt builder
|
||||
│ ├── tool-split.ts # Split tools into builtIn vs custom
|
||||
│ ├── types.ts # EmbeddedPiAgentMeta, EmbeddedPiRunResult
|
||||
│ └── utils.ts # ThinkLevel mapping, error description
|
||||
├── pi-embedded-subscribe.ts # Session event subscription/dispatch
|
||||
├── pi-embedded-subscribe.types.ts # SubscribeEmbeddedPiSessionParams
|
||||
├── pi-embedded-subscribe.handlers.ts # Event handler factory
|
||||
├── pi-embedded-subscribe.handlers.lifecycle.ts
|
||||
├── pi-embedded-subscribe.handlers.types.ts
|
||||
├── pi-embedded-block-chunker.ts # Streaming block reply chunking
|
||||
├── pi-embedded-messaging.ts # Messaging tool sent tracking
|
||||
├── pi-embedded-helpers.ts # Error classification, turn validation
|
||||
├── pi-embedded-helpers/ # Helper modules
|
||||
├── pi-embedded-utils.ts # Formatting utilities
|
||||
├── pi-tools.ts # createOpenClawCodingTools()
|
||||
├── pi-tools.abort.ts # AbortSignal wrapping for tools
|
||||
├── pi-tools.policy.ts # Tool allowlist/denylist policy
|
||||
├── pi-tools.read.ts # Read tool customizations
|
||||
├── pi-tools.schema.ts # Tool schema normalization
|
||||
├── pi-tools.types.ts # AnyAgentTool type alias
|
||||
├── pi-tool-definition-adapter.ts # AgentTool -> ToolDefinition adapter
|
||||
├── pi-settings.ts # Settings overrides
|
||||
├── pi-extensions/ # Custom pi extensions
|
||||
│ ├── compaction-safeguard.ts # Safeguard extension
|
||||
│ ├── compaction-safeguard-runtime.ts
|
||||
│ ├── context-pruning.ts # Cache-TTL context pruning extension
|
||||
│ └── context-pruning/
|
||||
├── model-auth.ts # Auth profile resolution
|
||||
├── auth-profiles.ts # Profile store, cooldown, failover
|
||||
├── model-selection.ts # Default model resolution
|
||||
├── models-config.ts # models.json generation
|
||||
├── model-catalog.ts # Model catalog cache
|
||||
├── context-window-guard.ts # Context window validation
|
||||
├── failover-error.ts # FailoverError class
|
||||
├── defaults.ts # DEFAULT_PROVIDER, DEFAULT_MODEL
|
||||
├── system-prompt.ts # buildAgentSystemPrompt()
|
||||
├── system-prompt-params.ts # System prompt parameter resolution
|
||||
├── system-prompt-report.ts # Debug report generation
|
||||
├── tool-summaries.ts # Tool description summaries
|
||||
├── tool-policy.ts # Tool policy resolution
|
||||
├── transcript-policy.ts # Transcript validation policy
|
||||
├── skills.ts # Skill snapshot/prompt building
|
||||
├── skills/ # Skill subsystem
|
||||
├── sandbox.ts # Sandbox context resolution
|
||||
├── sandbox/ # Sandbox subsystem
|
||||
├── channel-tools.ts # Channel-specific tool injection
|
||||
├── openclaw-tools.ts # OpenClaw-specific tools
|
||||
├── bash-tools.ts # exec/process tools
|
||||
├── apply-patch.ts # apply_patch tool (OpenAI)
|
||||
├── tools/ # Individual tool implementations
|
||||
│ ├── browser-tool.ts
|
||||
│ ├── canvas-tool.ts
|
||||
│ ├── cron-tool.ts
|
||||
│ ├── discord-actions*.ts
|
||||
│ ├── gateway-tool.ts
|
||||
│ ├── image-tool.ts
|
||||
│ ├── message-tool.ts
|
||||
│ ├── nodes-tool.ts
|
||||
│ ├── session*.ts
|
||||
│ ├── slack-actions.ts
|
||||
│ ├── telegram-actions.ts
|
||||
│ ├── web-*.ts
|
||||
│ └── whatsapp-actions.ts
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Core Integration Flow
|
||||
|
||||
### 1. Running an Embedded Agent
|
||||
|
||||
The main entry point is `runEmbeddedPiAgent()` in `pi-embedded-runner/run.ts`:
|
||||
|
||||
```typescript
|
||||
import { runEmbeddedPiAgent } from "./agents/pi-embedded-runner.js";
|
||||
|
||||
const result = await runEmbeddedPiAgent({
|
||||
sessionId: "user-123",
|
||||
sessionKey: "main:whatsapp:+1234567890",
|
||||
sessionFile: "/path/to/session.jsonl",
|
||||
workspaceDir: "/path/to/workspace",
|
||||
config: openclawConfig,
|
||||
prompt: "Hello, how are you?",
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
timeoutMs: 120_000,
|
||||
runId: "run-abc",
|
||||
onBlockReply: async (payload) => {
|
||||
await sendToChannel(payload.text, payload.mediaUrls);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Session Creation
|
||||
|
||||
Inside `runEmbeddedAttempt()` (called by `runEmbeddedPiAgent()`), the pi SDK is used:
|
||||
|
||||
```typescript
|
||||
import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
cwd: resolvedWorkspace,
|
||||
agentDir,
|
||||
authStorage: params.authStorage,
|
||||
modelRegistry: params.modelRegistry,
|
||||
model: params.model,
|
||||
thinkingLevel: mapThinkingLevel(params.thinkLevel),
|
||||
systemPrompt: createSystemPromptOverride(appendPrompt),
|
||||
tools: builtInTools,
|
||||
customTools: allCustomTools,
|
||||
sessionManager,
|
||||
settingsManager,
|
||||
skills: [],
|
||||
contextFiles: [],
|
||||
additionalExtensionPaths,
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Event Subscription
|
||||
|
||||
`subscribeEmbeddedPiSession()` subscribes to pi's `AgentSession` events:
|
||||
|
||||
```typescript
|
||||
const subscription = subscribeEmbeddedPiSession({
|
||||
session: activeSession,
|
||||
runId: params.runId,
|
||||
verboseLevel: params.verboseLevel,
|
||||
reasoningMode: params.reasoningLevel,
|
||||
toolResultFormat: params.toolResultFormat,
|
||||
onToolResult: params.onToolResult,
|
||||
onReasoningStream: params.onReasoningStream,
|
||||
onBlockReply: params.onBlockReply,
|
||||
onPartialReply: params.onPartialReply,
|
||||
onAgentEvent: params.onAgentEvent,
|
||||
});
|
||||
```
|
||||
|
||||
Events handled include:
|
||||
- `message_start` / `message_end` / `message_update` (streaming text/thinking)
|
||||
- `tool_execution_start` / `tool_execution_update` / `tool_execution_end`
|
||||
- `turn_start` / `turn_end`
|
||||
- `agent_start` / `agent_end`
|
||||
- `auto_compaction_start` / `auto_compaction_end`
|
||||
|
||||
### 4. Prompting
|
||||
|
||||
After setup, the session is prompted:
|
||||
|
||||
```typescript
|
||||
await session.prompt(effectivePrompt, { images: imageResult.images });
|
||||
```
|
||||
|
||||
The SDK handles the full agent loop: sending to LLM, executing tool calls, streaming responses.
|
||||
|
||||
## Tool Architecture
|
||||
|
||||
### Tool Pipeline
|
||||
|
||||
1. **Base Tools**: pi's `codingTools` (read, bash, edit, write)
|
||||
2. **Custom Replacements**: OpenClaw replaces bash with `exec`/`process`, customizes read/edit/write for sandbox
|
||||
3. **OpenClaw Tools**: messaging, browser, canvas, sessions, cron, gateway, etc.
|
||||
4. **Channel Tools**: Discord/Telegram/Slack/WhatsApp-specific action tools
|
||||
5. **Policy Filtering**: Tools filtered by profile, provider, agent, group, sandbox policies
|
||||
6. **Schema Normalization**: Schemas cleaned for Gemini/OpenAI quirks
|
||||
7. **AbortSignal Wrapping**: Tools wrapped to respect abort signals
|
||||
|
||||
### Tool Definition Adapter
|
||||
|
||||
pi-agent-core's `AgentTool` has a different `execute` signature than pi-coding-agent's `ToolDefinition`. The adapter in `pi-tool-definition-adapter.ts` bridges this:
|
||||
|
||||
```typescript
|
||||
export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
|
||||
return tools.map((tool) => ({
|
||||
name: tool.name,
|
||||
label: tool.label ?? name,
|
||||
description: tool.description ?? "",
|
||||
parameters: tool.parameters,
|
||||
execute: async (toolCallId, params, onUpdate, _ctx, signal) => {
|
||||
// pi-coding-agent signature differs from pi-agent-core
|
||||
return await tool.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Split Strategy
|
||||
|
||||
`splitSdkTools()` passes all tools via `customTools`:
|
||||
|
||||
```typescript
|
||||
export function splitSdkTools(options: { tools: AnyAgentTool[]; sandboxEnabled: boolean }) {
|
||||
return {
|
||||
builtInTools: [], // Empty. We override everything
|
||||
customTools: toToolDefinitions(options.tools),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This ensures OpenClaw's policy filtering, sandbox integration, and extended toolset remain consistent across providers.
|
||||
|
||||
## System Prompt Construction
|
||||
|
||||
The system prompt is built in `buildAgentSystemPrompt()` (`system-prompt.ts`). It assembles a full prompt with sections including Tooling, Tool Call Style, OpenClaw CLI reference, Skills, Docs, Workspace, Sandbox, Messaging, Reply Tags, Voice, Silent Replies, Heartbeats, Runtime metadata, plus Memory and Reactions when enabled, and optional context files and extra system prompt content. Sections are trimmed for minimal prompt mode used by subagents.
|
||||
|
||||
The prompt is passed to pi via `systemPrompt` override:
|
||||
|
||||
```typescript
|
||||
const systemPrompt = createSystemPromptOverride(appendPrompt);
|
||||
// Returns: (defaultPrompt: string) => trimmed custom prompt
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
### Session Files
|
||||
|
||||
Sessions are JSONL files with tree structure (id/parentId linking). Pi's `SessionManager` handles persistence:
|
||||
|
||||
```typescript
|
||||
const sessionManager = SessionManager.open(params.sessionFile);
|
||||
```
|
||||
|
||||
OpenClaw wraps this with `guardSessionManager()` for tool result safety.
|
||||
|
||||
### Session Caching
|
||||
|
||||
`session-manager-cache.ts` caches SessionManager instances to avoid repeated file parsing:
|
||||
|
||||
```typescript
|
||||
await prewarmSessionFile(params.sessionFile);
|
||||
sessionManager = SessionManager.open(params.sessionFile);
|
||||
trackSessionManagerAccess(params.sessionFile);
|
||||
```
|
||||
|
||||
### History Limiting
|
||||
|
||||
`limitHistoryTurns()` trims conversation history based on channel type (DM vs group).
|
||||
|
||||
### Compaction
|
||||
|
||||
Auto-compaction triggers on context overflow. `compactEmbeddedPiSessionDirect()` handles manual compaction:
|
||||
|
||||
```typescript
|
||||
const compactResult = await compactEmbeddedPiSessionDirect({
|
||||
sessionId, sessionFile, provider, model, ...
|
||||
});
|
||||
```
|
||||
|
||||
## Authentication & Model Resolution
|
||||
|
||||
### Auth Profiles
|
||||
|
||||
OpenClaw maintains an auth profile store with multiple API keys per provider:
|
||||
|
||||
```typescript
|
||||
const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
|
||||
const profileOrder = resolveAuthProfileOrder({ cfg, store: authStore, provider, preferredProfile });
|
||||
```
|
||||
|
||||
Profiles rotate on failures with cooldown tracking:
|
||||
|
||||
```typescript
|
||||
await markAuthProfileFailure({ store, profileId, reason, cfg, agentDir });
|
||||
const rotated = await advanceAuthProfile();
|
||||
```
|
||||
|
||||
### Model Resolution
|
||||
|
||||
```typescript
|
||||
import { resolveModel } from "./pi-embedded-runner/model.js";
|
||||
|
||||
const { model, error, authStorage, modelRegistry } = resolveModel(
|
||||
provider, modelId, agentDir, config
|
||||
);
|
||||
|
||||
// Uses pi's ModelRegistry and AuthStorage
|
||||
authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey);
|
||||
```
|
||||
|
||||
### Failover
|
||||
|
||||
`FailoverError` triggers model fallback when configured:
|
||||
|
||||
```typescript
|
||||
if (fallbackConfigured && isFailoverErrorMessage(errorText)) {
|
||||
throw new FailoverError(errorText, {
|
||||
reason: promptFailoverReason ?? "unknown",
|
||||
provider, model: modelId, profileId,
|
||||
status: resolveFailoverStatus(promptFailoverReason),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Pi Extensions
|
||||
|
||||
OpenClaw loads custom pi extensions for specialized behavior:
|
||||
|
||||
### Compaction Safeguard
|
||||
|
||||
`pi-extensions/compaction-safeguard.ts` adds guardrails to compaction, including adaptive token budgeting plus tool failure and file operation summaries:
|
||||
|
||||
```typescript
|
||||
if (resolveCompactionMode(params.cfg) === "safeguard") {
|
||||
setCompactionSafeguardRuntime(params.sessionManager, { maxHistoryShare });
|
||||
paths.push(resolvePiExtensionPath("compaction-safeguard"));
|
||||
}
|
||||
```
|
||||
|
||||
### Context Pruning
|
||||
|
||||
`pi-extensions/context-pruning.ts` implements cache-TTL based context pruning:
|
||||
|
||||
```typescript
|
||||
if (cfg?.agents?.defaults?.contextPruning?.mode === "cache-ttl") {
|
||||
setContextPruningRuntime(params.sessionManager, {
|
||||
settings, contextWindowTokens, isToolPrunable, lastCacheTouchAt,
|
||||
});
|
||||
paths.push(resolvePiExtensionPath("context-pruning"));
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming & Block Replies
|
||||
|
||||
### Block Chunking
|
||||
|
||||
`EmbeddedBlockChunker` manages streaming text into discrete reply blocks:
|
||||
|
||||
```typescript
|
||||
const blockChunker = blockChunking ? new EmbeddedBlockChunker(blockChunking) : null;
|
||||
```
|
||||
|
||||
### Thinking/Final Tag Stripping
|
||||
|
||||
Streaming output is processed to strip `<think>`/`<thinking>` blocks and extract `<final>` content:
|
||||
|
||||
```typescript
|
||||
const stripBlockTags = (text: string, state: { thinking: boolean; final: boolean }) => {
|
||||
// Strip <think>...</think> content
|
||||
// If enforceFinalTag, only return <final>...</final> content
|
||||
};
|
||||
```
|
||||
|
||||
### Reply Directives
|
||||
|
||||
Reply directives like `[[media:url]]`, `[[voice]]`, `[[reply:id]]` are parsed and extracted:
|
||||
|
||||
```typescript
|
||||
const { text: cleanedText, mediaUrls, audioAsVoice, replyToId } = consumeReplyDirectives(chunk);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Classification
|
||||
|
||||
`pi-embedded-helpers.ts` classifies errors for appropriate handling:
|
||||
|
||||
```typescript
|
||||
isContextOverflowError(errorText) // Context too large
|
||||
isCompactionFailureError(errorText) // Compaction failed
|
||||
isAuthAssistantError(lastAssistant) // Auth failure
|
||||
isRateLimitAssistantError(...) // Rate limited
|
||||
isFailoverAssistantError(...) // Should failover
|
||||
classifyFailoverReason(errorText) // "auth" | "rate_limit" | "quota" | "timeout" | ...
|
||||
```
|
||||
|
||||
### Thinking Level Fallback
|
||||
|
||||
If a thinking level is unsupported, it falls back:
|
||||
|
||||
```typescript
|
||||
const fallbackThinking = pickFallbackThinkingLevel({
|
||||
message: errorText,
|
||||
attempted: attemptedThinking,
|
||||
});
|
||||
if (fallbackThinking) {
|
||||
thinkLevel = fallbackThinking;
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
## Sandbox Integration
|
||||
|
||||
When sandbox mode is enabled, tools and paths are constrained:
|
||||
|
||||
```typescript
|
||||
const sandbox = await resolveSandboxContext({
|
||||
config: params.config,
|
||||
sessionKey: sandboxSessionKey,
|
||||
workspaceDir: resolvedWorkspace,
|
||||
});
|
||||
|
||||
if (sandboxRoot) {
|
||||
// Use sandboxed read/edit/write tools
|
||||
// Exec runs in container
|
||||
// Browser uses bridge URL
|
||||
}
|
||||
```
|
||||
|
||||
## Provider-Specific Handling
|
||||
|
||||
### Anthropic
|
||||
|
||||
- Refusal magic string scrubbing
|
||||
- Turn validation for consecutive roles
|
||||
- Claude Code parameter compatibility
|
||||
|
||||
### Google/Gemini
|
||||
|
||||
- Turn ordering fixes (`applyGoogleTurnOrderingFix`)
|
||||
- Tool schema sanitization (`sanitizeToolsForGoogle`)
|
||||
- Session history sanitization (`sanitizeSessionHistory`)
|
||||
|
||||
### OpenAI
|
||||
|
||||
- `apply_patch` tool for Codex models
|
||||
- Thinking level downgrade handling
|
||||
|
||||
## TUI Integration
|
||||
|
||||
OpenClaw also has a local TUI mode that uses pi-tui components directly:
|
||||
|
||||
```typescript
|
||||
// src/tui/tui.ts
|
||||
import { ... } from "@mariozechner/pi-tui";
|
||||
```
|
||||
|
||||
This provides the interactive terminal experience similar to pi's native mode.
|
||||
|
||||
## Key Differences from Pi CLI
|
||||
|
||||
| Aspect | Pi CLI | OpenClaw Embedded |
|
||||
|--------|--------|-------------------|
|
||||
| Invocation | `pi` command / RPC | SDK via `createAgentSession()` |
|
||||
| Tools | Default coding tools | Custom OpenClaw tool suite |
|
||||
| System prompt | AGENTS.md + prompts | Dynamic per-channel/context |
|
||||
| Session storage | `~/.pi/agent/sessions/` | `~/.openclaw/agents/<agentId>/sessions/` (or `$OPENCLAW_STATE_DIR/agents/<agentId>/sessions/`) |
|
||||
| Auth | Single credential | Multi-profile with rotation |
|
||||
| Extensions | Loaded from disk | Programmatic + disk paths |
|
||||
| Event handling | TUI rendering | Callback-based (onBlockReply, etc.) |
|
||||
|
||||
## Future Considerations
|
||||
|
||||
Areas for potential rework:
|
||||
|
||||
1. **Tool signature alignment**: Currently adapting between pi-agent-core and pi-coding-agent signatures
|
||||
2. **Session manager wrapping**: `guardSessionManager` adds safety but increases complexity
|
||||
3. **Extension loading**: Could use pi's `ResourceLoader` more directly
|
||||
4. **Streaming handler complexity**: `subscribeEmbeddedPiSession` has grown large
|
||||
5. **Provider quirks**: Many provider-specific codepaths that pi could potentially handle
|
||||
|
||||
## Tests
|
||||
|
||||
All existing tests that cover the pi integration and its extensions:
|
||||
|
||||
- `src/agents/pi-embedded-block-chunker.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.downgradeopenai-reasoning.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.formatrawassistanterrorforui.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.image-dimension-error.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.image-size-error.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.isautherrormessage.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.iscompactionfailureerror.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.iscontextoverflowerror.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.isfailovererrormessage.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.islikelycontextoverflowerror.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.ismessagingtoolduplicate.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.messaging-duplicate.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.normalizetextforcomparison.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.sanitizegoogleturnordering.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.sanitizetoolcallid.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.stripthoughtsignatures.test.ts`
|
||||
- `src/agents/pi-embedded-helpers.validate-turns.test.ts`
|
||||
- `src/agents/pi-embedded-runner-extraparams.live.test.ts` (live)
|
||||
- `src/agents/pi-embedded-runner-extraparams.test.ts`
|
||||
- `src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts`
|
||||
- `src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts`
|
||||
- `src/agents/pi-embedded-runner.createsystempromptoverride.test.ts`
|
||||
- `src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts`
|
||||
- `src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts`
|
||||
- `src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts`
|
||||
- `src/agents/pi-embedded-runner.guard.test.ts`
|
||||
- `src/agents/pi-embedded-runner.limithistoryturns.test.ts`
|
||||
- `src/agents/pi-embedded-runner.resolvesessionagentids.test.ts`
|
||||
- `src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.test.ts`
|
||||
- `src/agents/pi-embedded-runner.sanitize-session-history.test.ts`
|
||||
- `src/agents/pi-embedded-runner.splitsdktools.test.ts`
|
||||
- `src/agents/pi-embedded-runner.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.code-span-awareness.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.reply-tags.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts`
|
||||
- `src/agents/pi-embedded-subscribe.tools.test.ts`
|
||||
- `src/agents/pi-embedded-utils.test.ts`
|
||||
- `src/agents/pi-extensions/compaction-safeguard.test.ts`
|
||||
- `src/agents/pi-extensions/context-pruning.test.ts`
|
||||
- `src/agents/pi-settings.test.ts`
|
||||
- `src/agents/pi-tool-definition-adapter.test.ts`
|
||||
- `src/agents/pi-tools-agent-config.test.ts`
|
||||
- `src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts`
|
||||
- `src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts`
|
||||
- `src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.test.ts`
|
||||
- `src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts`
|
||||
- `src/agents/pi-tools.policy.test.ts`
|
||||
- `src/agents/pi-tools.safe-bins.test.ts`
|
||||
- `src/agents/pi-tools.workspace-paths.test.ts`
|
||||
@@ -97,7 +97,7 @@
|
||||
"gateway:dev": "OPENCLAW_SKIP_CHANNELS=1 CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway",
|
||||
"gateway:dev:reset": "OPENCLAW_SKIP_CHANNELS=1 CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",
|
||||
"tui": "node scripts/run-node.mjs tui",
|
||||
"tui:dev": "OPENCLAW_PROFILE=dev CLAWDBOT_PROFILE=dev node scripts/run-node.mjs tui",
|
||||
"tui:dev": "OPENCLAW_PROFILE=dev CLAWDBOT_PROFILE=dev node scripts/run-node.mjs --dev tui",
|
||||
"openclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
|
||||
"moltbot:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
|
||||
"ios:gen": "cd apps/ios && xcodegen generate",
|
||||
|
||||
Reference in New Issue
Block a user