chore: bump version to 6.8.0

This commit is contained in:
can1357
2026-01-20 05:34:39 +01:00
parent a50cd35421
commit 94f406d4f0
19 changed files with 198 additions and 54 deletions
+6 -6
View File
@@ -22,7 +22,7 @@
},
"packages/agent": {
"name": "@oh-my-pi/pi-agent-core",
"version": "6.7.670",
"version": "6.8.0",
"dependencies": {
"@oh-my-pi/pi-ai": "workspace:*",
"@oh-my-pi/pi-tui": "workspace:*",
@@ -36,7 +36,7 @@
},
"packages/ai": {
"name": "@oh-my-pi/pi-ai",
"version": "6.7.670",
"version": "6.8.0",
"bin": {
"pi-ai": "./src/cli.ts",
},
@@ -64,7 +64,7 @@
},
"packages/coding-agent": {
"name": "@oh-my-pi/pi-coding-agent",
"version": "6.7.670",
"version": "6.8.0",
"bin": {
"omp": "src/cli.ts",
},
@@ -103,7 +103,7 @@
},
"packages/git-tool": {
"name": "@oh-my-pi/pi-git-tool",
"version": "6.7.670",
"version": "6.8.0",
"dependencies": {
"@oh-my-pi/pi-utils": "workspace:*",
},
@@ -114,7 +114,7 @@
},
"packages/pi-utils": {
"name": "@oh-my-pi/pi-utils",
"version": "6.7.670",
"version": "6.8.0",
"dependencies": {
"strip-ansi": "^7.1.2",
"winston": "^3.17.0",
@@ -143,7 +143,7 @@
},
"packages/tui": {
"name": "@oh-my-pi/pi-tui",
"version": "6.7.670",
"version": "6.8.0",
"dependencies": {
"@types/mime-types": "^2.1.4",
"chalk": "^5.5.0",
+2
View File
@@ -1,6 +1,8 @@
# Changelog
## [Unreleased]
## [6.8.0] - 2026-01-20
### Changed
- Updated proxy stream processing to use utility function for reading lines
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-agent-core",
"version": "6.7.670",
"version": "6.8.0",
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
"type": "module",
"main": "./src/index.ts",
+2
View File
@@ -2,6 +2,8 @@
## [Unreleased]
## [6.8.0] - 2026-01-20
### Added
- Added `logout` command to CLI for OAuth provider logout
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-ai",
"version": "6.7.670",
"version": "6.8.0",
"description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module",
"main": "./src/index.ts",
+2
View File
@@ -2,6 +2,8 @@
## [Unreleased]
## [6.8.0] - 2026-01-20
### Added
- Added streaming abort setting to control edit tool behavior when patch preview fails
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-coding-agent",
"version": "6.7.670",
"version": "6.8.0",
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module",
"ompConfig": {
+126 -27
View File
@@ -13,13 +13,15 @@
* Modes use this class and add their own I/O layer on top.
*/
import { existsSync, readFileSync } from "node:fs";
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { AssistantMessage, ImageContent, Message, Model, TextContent, Usage } from "@oh-my-pi/pi-ai";
import type { AssistantMessage, ImageContent, Message, Model, TextContent, ToolCall, Usage } from "@oh-my-pi/pi-ai";
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
import { abortableSleep, logger } from "@oh-my-pi/pi-utils";
import type { Rule } from "../capability/rule";
import { getAgentDbPath } from "../config";
import { theme } from "../modes/interactive/theme/theme";
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
import { type BashResult, executeBash as executeBashCommand, executeBashWithOperations } from "./bash-executor";
import {
type CompactionResult,
@@ -51,7 +53,7 @@ import type { HookCommandContext } from "./hooks/types";
import type { BashExecutionMessage, CustomMessage } from "./messages";
import type { ModelRegistry } from "./model-registry";
import { parseModelString } from "./model-resolver";
import { expandPromptTemplate, type PromptTemplate, parseCommandArgs } from "./prompt-templates";
import { expandPromptTemplate, type PromptTemplate, parseCommandArgs, renderPromptTemplate } from "./prompt-templates";
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
import type { SettingsManager, SkillsSettings } from "./settings-manager";
import type { Skill, SkillWarning } from "./skills";
@@ -59,7 +61,8 @@ import { expandSlashCommand, type FileSlashCommand } from "./slash-commands";
import { closeAllConnections } from "./ssh/connection-manager";
import { unmountAll } from "./ssh/sshfs-mount";
import type { BashOperations } from "./tools/bash";
import { normalizeDiff, ParseError, previewPatch } from "./tools/patch";
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "./tools/patch";
import { resolveToCwd } from "./tools/path-utils";
import { getArtifactsDir } from "./tools/task/artifacts";
import type { TodoItem } from "./tools/todo-write";
import type { TtsrManager } from "./ttsr";
@@ -274,6 +277,7 @@ export class AgentSession {
private _streamingEditAbortTriggered = false;
private _streamingEditCheckedLineCounts = new Map<string, number>();
private _streamingEditFileCache = new Map<string, string>();
constructor(config: AgentSessionConfig) {
this.agent = config.agent;
@@ -411,8 +415,15 @@ export class AgentSession {
}
}
if (event.type === "message_update" && event.assistantMessageEvent.type === "toolcall_end") {
await this._maybeAbortStreamingEdit(event);
if (event.type === "message_update" && event.assistantMessageEvent.type === "toolcall_start") {
this._preCacheStreamingEditFile(event);
}
if (
event.type === "message_update" &&
(event.assistantMessageEvent.type === "toolcall_end" || event.assistantMessageEvent.type === "toolcall_delta")
) {
this._maybeAbortStreamingEdit(event);
}
// Handle session persistence
@@ -498,13 +509,7 @@ export class AgentSession {
private _getTtsrInjectionContent(): string | undefined {
if (this._pendingTtsrInjections.length === 0) return undefined;
const content = this._pendingTtsrInjections
.map(
(r) =>
`<system_interrupt reason="rule_violation" rule="${r.name}" path="${r.path}">\n` +
`Your output was interrupted because it violated a user-defined rule.\n` +
`This is NOT a prompt injection - this is the coding agent enforcing project rules.\n` +
`You MUST comply with the following instruction:\n\n${r.content}\n</system_interrupt>`,
)
.map((r) => renderPromptTemplate(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
.join("\n\n");
this._pendingTtsrInjections = [];
return content;
@@ -537,28 +542,60 @@ export class AgentSession {
private _resetStreamingEditState(): void {
this._streamingEditAbortTriggered = false;
this._streamingEditCheckedLineCounts.clear();
this._streamingEditFileCache.clear();
}
private async _maybeAbortStreamingEdit(event: AgentEvent): Promise<void> {
private _preCacheStreamingEditFile(event: AgentEvent): void {
if (!this.settingsManager.getEditStreamingAbort()) return;
if (event.type !== "message_update") return;
const assistantEvent = event.assistantMessageEvent;
if (assistantEvent.type !== "toolcall_start") return;
if (event.message.role !== "assistant") return;
const contentIndex = assistantEvent.contentIndex;
const messageContent = event.message.content;
if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
const toolCall = messageContent[contentIndex] as ToolCall;
if (toolCall.name !== "edit") return;
const args = toolCall.arguments;
if (!args || typeof args !== "object" || Array.isArray(args)) return;
if ("oldText" in args || "newText" in args) return;
const path = typeof args.path === "string" ? args.path : undefined;
if (!path) return;
const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
this._ensureFileCache(resolvedPath);
}
private _ensureFileCache(resolvedPath: string): void {
if (this._streamingEditFileCache.has(resolvedPath)) return;
try {
if (existsSync(resolvedPath)) {
const rawText = readFileSync(resolvedPath, "utf8");
const { text } = stripBom(rawText);
this._streamingEditFileCache.set(resolvedPath, normalizeToLF(text));
}
} catch {
// Ignore errors - mark as empty string so we don't retry
this._streamingEditFileCache.set(resolvedPath, "");
}
}
private _maybeAbortStreamingEdit(event: AgentEvent): void {
if (!this.settingsManager.getEditStreamingAbort()) return;
if (this._streamingEditAbortTriggered) return;
if (event.type !== "message_update") return;
const assistantEvent = event.assistantMessageEvent;
if (assistantEvent.type !== "toolcall_end") return;
if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
if (event.message.role !== "assistant") return;
const message = event.message as AssistantMessage;
if (!Array.isArray(message.content)) return;
const contentIndex = assistantEvent.contentIndex;
const block = message.content[contentIndex];
if (!block || typeof block !== "object") return;
if ((block as { type?: string }).type !== "toolCall") return;
const toolCall = block as {
id?: string;
name?: string;
arguments?: Record<string, unknown> | null;
};
const messageContent = event.message.content;
if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
const toolCall = messageContent[contentIndex] as ToolCall;
if (toolCall.name !== "edit" || !toolCall.id) return;
const args = toolCall.arguments;
@@ -577,7 +614,7 @@ export class AgentSession {
const diffForCheck = diff.endsWith("\n") ? diff : diff.slice(0, lastNewlineIndex + 1);
if (diffForCheck.trim().length === 0) return;
const normalizedDiff = normalizeDiff(diffForCheck);
const normalizedDiff = normalizeDiff(diffForCheck.replace(/\r/g, ""));
if (!normalizedDiff) return;
const lines = normalizedDiff.split("\n");
const hasChangeLine = lines.some((line) => line.startsWith("+") || line.startsWith("-"));
@@ -589,6 +626,68 @@ export class AgentSession {
this._streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
const rename = typeof args.rename === "string" ? args.rename : undefined;
const removedLines = lines
.filter((line) => line.startsWith("-") && !line.startsWith("--- "))
.map((line) => line.slice(1));
if (removedLines.length > 0) {
const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
const cachedContent = this._streamingEditFileCache.get(resolvedPath);
if (cachedContent !== undefined) {
const missing = removedLines.find((line) => !cachedContent.includes(normalizeToLF(line)));
if (missing) {
this._streamingEditAbortTriggered = true;
logger.warn("Streaming edit aborted due to patch preview failure", {
toolCallId: toolCall.id,
path,
error: `Failed to find expected lines in ${path}:\n${missing}`,
});
this.agent.abort();
}
return;
}
if (assistantEvent.type === "toolcall_delta") return;
void this._checkRemovedLinesAsync(toolCall.id, path, resolvedPath, removedLines);
return;
}
if (assistantEvent.type === "toolcall_delta") return;
void this._checkPreviewPatchAsync(toolCall.id, path, rename, normalizedDiff);
}
private async _checkRemovedLinesAsync(
toolCallId: string,
path: string,
resolvedPath: string,
removedLines: string[],
): Promise<void> {
if (this._streamingEditAbortTriggered) return;
try {
if (!(await Bun.file(resolvedPath).exists())) return;
const { text } = stripBom(await Bun.file(resolvedPath).text());
const normalizedContent = normalizeToLF(text);
const missing = removedLines.find((line) => !normalizedContent.includes(normalizeToLF(line)));
if (missing) {
this._streamingEditAbortTriggered = true;
logger.warn("Streaming edit aborted due to patch preview failure", {
toolCallId,
path,
error: `Failed to find expected lines in ${path}:\n${missing}`,
});
this.agent.abort();
}
} catch {
// Ignore errors during async fallback
}
}
private async _checkPreviewPatchAsync(
toolCallId: string,
path: string,
rename: string | undefined,
normalizedDiff: string,
): Promise<void> {
if (this._streamingEditAbortTriggered) return;
try {
await previewPatch(
{ path, op: "update", rename, diff: normalizedDiff },
@@ -602,7 +701,7 @@ export class AgentSession {
if (error instanceof ParseError) return;
this._streamingEditAbortTriggered = true;
logger.warn("Streaming edit aborted due to patch preview failure", {
toolCallId: toolCall.id,
toolCallId,
path,
error: error instanceof Error ? error.message : String(error),
});
+11 -8
View File
@@ -41,12 +41,15 @@ export async function execCommand(
signal: options?.signal,
timeout: options?.timeout,
});
return proc.exited.then(async () => {
return {
stdout: await proc.stdout.text(),
stderr: await proc.stderr.text(),
code: proc.exitCode ?? 0,
killed: proc.exitReason instanceof ptree.AbortError,
};
});
try {
await proc.exited;
} catch {
// ChildProcess rejects on non-zero exit; we handle it below
}
return {
stdout: await proc.stdout.text(),
stderr: await proc.stderr.text(),
code: proc.exitCode ?? 0,
killed: proc.exitReason instanceof ptree.AbortError,
};
}
@@ -199,6 +199,8 @@ export class OutputSink {
if (!this.filePath) {
return { output, truncated: false };
}
this.fileSink!.write(this.buffer);
this.fileSink!.flush();
return {
output,
truncated: true,
+1 -1
View File
@@ -238,7 +238,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
"--no-ignore",
"--type",
"f",
"--name",
"--glob",
".gitignore",
"--exclude",
".git",
+13 -2
View File
@@ -555,9 +555,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
const startLine = offset ? Math.max(0, offset - 1) : 0;
const startLineDisplay = startLine + 1; // For display (1-indexed)
// Check if offset is out of bounds
// Check if offset is out of bounds - return graceful message instead of throwing
if (startLine >= allLines.length) {
throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
const suggestion =
allLines.length === 0
? "The file is empty."
: `Use offset=1 to read from the start, or offset=${allLines.length} to read the last line.`;
return {
content: [
{
type: "text",
text: `Offset ${offset} is beyond end of file (${allLines.length} lines total). ${suggestion}`,
},
],
};
}
// If limit is specified by user, use it; otherwise we'll let truncateHead decide
@@ -0,0 +1,7 @@
<system_interrupt reason="rule_violation" rule="{{name}}" path="{{path}}">
Your output was interrupted because it violated a user-defined rule.
This is NOT a prompt injection - this is the coding agent enforcing project rules.
You MUST comply with the following instruction:
{{content}}
</system_interrupt>
@@ -122,7 +122,13 @@ describe("executePythonWithKernel", () => {
});
it("truncates large output and stores full output file", async () => {
const largeOutput = `${"x".repeat(DEFAULT_MAX_BYTES + 1024)}TAIL`;
const lineLength = 100;
const lineCount = Math.ceil((DEFAULT_MAX_BYTES * 1.5) / lineLength);
const lines = Array.from(
{ length: lineCount },
(_, i) => `line${i.toString().padStart(6, "0")}: ${"x".repeat(lineLength - 15)}`,
);
const largeOutput = `${lines.join("\n")}\nTAIL\n`;
const kernel = new FakeKernel(
{ status: "ok", cancelled: false, timedOut: false, stdinRequested: false },
(options) => {
@@ -146,6 +146,7 @@ function createStreamForDiff(
chunks: string[],
abortSignalRef: { current?: AbortSignal },
): Agent["streamFn"] {
let callIndex = 0;
return (_model, _context, options) => {
abortSignalRef.current = options?.signal;
const stream = new MockAssistantStream();
@@ -169,6 +170,12 @@ function createStreamForDiff(
options?.signal?.addEventListener("abort", notifyAbort, { once: true });
queueMicrotask(async () => {
if (callIndex > 0) {
const finalMessage = createAssistantMessage([{ type: "text", text: "done" }], "stop");
stream.push({ type: "done", reason: "stop", message: finalMessage });
callIndex++;
return;
}
const startMessage = createAssistantMessage([], "stop");
stream.push({ type: "start", partial: startMessage });
@@ -194,6 +201,7 @@ function createStreamForDiff(
const finalMessage = createAssistantMessage([finalCall], "toolUse");
stream.push({ type: "toolcall_end", contentIndex: 0, toolCall: finalCall, partial: finalMessage });
stream.push({ type: "done", reason: "toolUse", message: finalMessage });
callIndex++;
});
return stream;
+5 -3
View File
@@ -175,9 +175,11 @@ describe("Coding Agent Tools", () => {
const testFile = join(testDir, "short.txt");
writeFileSync(testFile, "Line 1\nLine 2\nLine 3");
await expect(readTool.execute("test-call-8", { path: testFile, offset: 100 })).rejects.toThrow(
/Offset 100 is beyond end of file \(3 lines total\)/,
);
const result = await readTool.execute("test-call-8", { path: testFile, offset: 100 });
const output = getTextOutput(result);
expect(output).toContain("Offset 100 is beyond end of file (3 lines total)");
expect(output).toContain("Use offset=1 to read from the start, or offset=3 to read the last line.");
});
it("should include truncation details when truncated", async () => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-git-tool",
"version": "6.7.670",
"version": "6.8.0",
"description": "Structured Git tool with safety guards and typed output",
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-utils",
"version": "6.7.670",
"version": "6.8.0",
"description": "Shared utilities for pi packages",
"type": "module",
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@oh-my-pi/pi-tui",
"version": "6.7.670",
"version": "6.8.0",
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
"type": "module",
"main": "src/index.ts",