chore: bump version to 6.8.0
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,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,6 +2,8 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [6.8.0] - 2026-01-20
|
||||
|
||||
### Added
|
||||
|
||||
- Added `logout` command to CLI for OAuth provider logout
|
||||
|
||||
@@ -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,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,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": {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -238,7 +238,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
||||
"--no-ignore",
|
||||
"--type",
|
||||
"f",
|
||||
"--name",
|
||||
"--glob",
|
||||
".gitignore",
|
||||
"--exclude",
|
||||
".git",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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,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,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",
|
||||
|
||||
Reference in New Issue
Block a user