feat: removed normative rewrite experiment from public API and patch processing
- Removed normativeRewrite setting and buildNormativeUpdateInput() function from public API. - Removed $normative property and TNormative generic parameter from ToolResultMessage and AgentToolResult interfaces. - Deleted normative.ts patch normalization module and related helper functions for diff anchor processing. - Removed rewriteAssistantToolCallArgs() and #rewriteToolCallArgs() methods that modified tool call arguments.
This commit is contained in:
@@ -178,19 +178,15 @@ export interface AgentState {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface AgentToolResult<T = any, TNormative extends TSchema = any> {
|
||||
export interface AgentToolResult<T = any, _TInput = unknown> {
|
||||
// Content blocks supporting text and images
|
||||
content: (TextContent | ImageContent)[];
|
||||
// Details to be displayed in a UI or logged
|
||||
details?: T;
|
||||
/** Normative input for the tool result */
|
||||
$normative?: Static<TNormative>;
|
||||
}
|
||||
|
||||
// Callback for streaming tool execution updates
|
||||
export type AgentToolUpdateCallback<T = any, TNormative extends TSchema = any> = (
|
||||
partialResult: AgentToolResult<T, TNormative>,
|
||||
) => void;
|
||||
export type AgentToolUpdateCallback<T = any, TInput = unknown> = (partialResult: AgentToolResult<T, TInput>) => void;
|
||||
|
||||
/** Options passed to renderResult */
|
||||
export interface RenderResultOptions {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed `TInput` generic parameter from `ToolResultMessage` interface and removed `$normative` property
|
||||
|
||||
### Added
|
||||
|
||||
- `hasUnrepresentableStrictObjectMap()` pre-flight check in `tryEnforceStrictSchema`: schemas with `patternProperties` or schema-valued `additionalProperties` now degrade gracefully to non-strict mode instead of throwing during enforcement
|
||||
|
||||
@@ -280,7 +280,7 @@ export interface AssistantMessage {
|
||||
ttft?: number; // Time to first token in milliseconds
|
||||
}
|
||||
|
||||
export interface ToolResultMessage<TDetails = any, TInput = unknown> {
|
||||
export interface ToolResultMessage<TDetails = any> {
|
||||
role: "toolResult";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
@@ -290,7 +290,6 @@ export interface ToolResultMessage<TDetails = any, TInput = unknown> {
|
||||
/** Timestamp when output was pruned (ms since epoch). Undefined if unpruned. */
|
||||
prunedAt?: number;
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
$normative?: TInput;
|
||||
}
|
||||
|
||||
export type Message = UserMessage | DeveloperMessage | AssistantMessage | ToolResultMessage;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Breaking Changes
|
||||
|
||||
- `ast_find` parameter `pattern` (string) replaced by `patterns` (string[])
|
||||
- `ast_replace` parameters `pattern` + `rewrite` replaced by `ops: Array<{ pat: string; out: string }>`
|
||||
|
||||
### Added
|
||||
|
||||
@@ -18,17 +22,17 @@
|
||||
- Web search schema exposes `max_tokens`, `temperature`, and `num_search_results` as tool parameters
|
||||
- Web search provider fallback: when an explicit provider is unavailable, resolves the auto chain instead of returning empty results
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `normativeRewrite` setting that rewrote tool call arguments to normalized format in session history
|
||||
- Removed `buildNormativeUpdateInput()` helper and normative patch transformation logic
|
||||
|
||||
### Fixed
|
||||
|
||||
- `ast_replace` no longer rejects empty `out` values; an empty string now deletes matched nodes
|
||||
- `ast_replace` no longer trims `pat` and `out` values, preserving intentional whitespace
|
||||
- `gemini_image` tool: corrected `responseModalities` values from `'Image'`/`'Text'` to uppercase `'IMAGE'`/`'TEXT'` matching the API enum
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- `ast_find` parameter `pattern` (string) replaced by `patterns` (string[])
|
||||
- `ast_replace` parameters `pattern` + `rewrite` replaced by `ops: Array<{ pat: string; out: string }>`
|
||||
|
||||
## [13.3.14] - 2026-02-28
|
||||
|
||||
### Added
|
||||
@@ -5389,4 +5393,4 @@ Initial public release.
|
||||
- Git branch display in footer
|
||||
- Message queueing during streaming responses
|
||||
- OAuth integration for Gmail and Google Calendar access
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
@@ -254,15 +254,6 @@ export const SETTINGS_SCHEMA = {
|
||||
submenu: true,
|
||||
},
|
||||
},
|
||||
normativeRewrite: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
ui: {
|
||||
tab: "agent",
|
||||
label: "Normative rewrite",
|
||||
description: "Rewrite tool call arguments to normalized format in session history",
|
||||
},
|
||||
},
|
||||
repeatToolDescriptions: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Normalize applied patch output into a canonical edit tool payload.
|
||||
*/
|
||||
import { generateUnifiedDiffString } from "./diff";
|
||||
import { normalizeToLF, stripBom } from "./normalize";
|
||||
import { parseHunks } from "./parser";
|
||||
import type { PatchInput } from "./types";
|
||||
|
||||
export interface NormativePatchOptions {
|
||||
path: string;
|
||||
rename?: string;
|
||||
oldContent: string;
|
||||
newContent: string;
|
||||
contextLines?: number;
|
||||
anchor?: string | string[];
|
||||
}
|
||||
|
||||
/** Normative patch input is the MongoDB-style update variant */
|
||||
|
||||
function applyAnchors(diff: string, anchors: Array<string | undefined> | undefined): string {
|
||||
if (!anchors || anchors.length === 0) {
|
||||
return diff;
|
||||
}
|
||||
const lines = diff.split("\n");
|
||||
let anchorIndex = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!lines[i].startsWith("@@")) continue;
|
||||
const anchor = anchors[anchorIndex];
|
||||
if (anchor !== undefined) {
|
||||
lines[i] = anchor.trim().length === 0 ? "@@" : `@@ ${anchor}`;
|
||||
}
|
||||
anchorIndex++;
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function deriveAnchors(diff: string): Array<string | undefined> {
|
||||
const hunks = parseHunks(diff);
|
||||
return hunks.map(hunk => {
|
||||
if (hunk.oldLines.length === 0 || hunk.newLines.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const newLines = new Set(hunk.newLines);
|
||||
for (const line of hunk.oldLines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.length === 0) continue;
|
||||
if (!/[A-Za-z0-9_]/.test(trimmed)) continue;
|
||||
if (newLines.has(line)) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function buildNormativeUpdateInput(options: NormativePatchOptions): PatchInput {
|
||||
const normalizedOld = normalizeToLF(stripBom(options.oldContent).text);
|
||||
const normalizedNew = normalizeToLF(stripBom(options.newContent).text);
|
||||
const diffResult = generateUnifiedDiffString(normalizedOld, normalizedNew, options.contextLines ?? 3);
|
||||
let anchors: Array<string | undefined> | undefined =
|
||||
typeof options.anchor === "string" ? [options.anchor] : options.anchor;
|
||||
if (!anchors) {
|
||||
anchors = deriveAnchors(diffResult.diff);
|
||||
}
|
||||
const diff = applyAnchors(diffResult.diff, anchors);
|
||||
return {
|
||||
path: options.path,
|
||||
op: "update",
|
||||
rename: options.rename,
|
||||
diff,
|
||||
};
|
||||
}
|
||||
@@ -651,17 +651,12 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
if (event.message.role === "toolResult") {
|
||||
const { toolName, $normative, toolCallId, details, isError, content } = event.message as {
|
||||
const { toolName, details, isError, content } = event.message as {
|
||||
toolName?: string;
|
||||
toolCallId?: string;
|
||||
details?: { path?: string; phases?: TodoPhase[] };
|
||||
$normative?: Record<string, unknown>;
|
||||
isError?: boolean;
|
||||
content?: Array<TextContent | ImageContent>;
|
||||
};
|
||||
if ($normative && toolCallId && this.settings.get("normativeRewrite")) {
|
||||
await this.#rewriteToolCallArgs(toolCallId, $normative);
|
||||
}
|
||||
// Invalidate streaming edit cache when edit tool completes to prevent stale data
|
||||
if (toolName === "edit" && details?.path) {
|
||||
this.#invalidateFileCacheForPath(details.path);
|
||||
@@ -1259,33 +1254,6 @@ export class AgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
/** Rewrite tool call arguments in agent state and persisted session history. */
|
||||
async #rewriteToolCallArgs(toolCallId: string, args: Record<string, unknown>): Promise<void> {
|
||||
let updated = false;
|
||||
const messages = this.agent.state.messages;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg.role !== "assistant") continue;
|
||||
const assistantMsg = msg as AssistantMessage;
|
||||
if (!Array.isArray(assistantMsg.content)) continue;
|
||||
for (const block of assistantMsg.content) {
|
||||
if (typeof block !== "object" || block === null) continue;
|
||||
if (!("type" in block) || (block as { type?: string }).type !== "toolCall") continue;
|
||||
const toolCall = block as { id?: string; arguments?: Record<string, unknown> };
|
||||
if (toolCall.id === toolCallId) {
|
||||
toolCall.arguments = args;
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (updated) break;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
await this.sessionManager.rewriteAssistantToolCallArgs(toolCallId, args);
|
||||
}
|
||||
}
|
||||
|
||||
/** Emit extension events based on session events */
|
||||
async #emitExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
||||
if (!this.#extensionRunner) return;
|
||||
|
||||
@@ -1819,36 +1819,6 @@ export class SessionManager {
|
||||
await this.#rewriteFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite tool call arguments in the most recent assistant message containing the toolCallId.
|
||||
* Returns true if a tool call was updated.
|
||||
*/
|
||||
async rewriteAssistantToolCallArgs(toolCallId: string, args: Record<string, unknown>): Promise<boolean> {
|
||||
let updated = false;
|
||||
for (let i = this.#fileEntries.length - 1; i >= 0; i--) {
|
||||
const entry = this.#fileEntries[i];
|
||||
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
||||
const message = entry.message as { content?: unknown };
|
||||
if (!Array.isArray(message.content)) continue;
|
||||
for (const block of message.content) {
|
||||
if (typeof block !== "object" || block === null) continue;
|
||||
if (!("type" in block) || (block as { type?: string }).type !== "toolCall") continue;
|
||||
const toolCall = block as { id?: string; arguments?: Record<string, unknown> };
|
||||
if (toolCall.id === toolCallId) {
|
||||
toolCall.arguments = args;
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (updated) break;
|
||||
}
|
||||
|
||||
if (updated && this.persist && this.#sessionFile) {
|
||||
await this.#rewriteFile();
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a custom message entry (for extensions) that participates in LLM context.
|
||||
* @param customType Hook identifier for filtering on reload
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import type { AssistantMessage, ToolCall } from "@oh-my-pi/pi-ai";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
|
||||
function buildAssistantMessage(content: AssistantMessage["content"]): AssistantMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content,
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
model: "test",
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 1,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 2,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: 1,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SessionManager tool call rewrite", () => {
|
||||
it("rewrites assistant toolCall arguments in context", async () => {
|
||||
const session = SessionManager.inMemory();
|
||||
const toolCallId = "toolu_rewrite_1";
|
||||
|
||||
const toolCall = {
|
||||
type: "toolCall",
|
||||
id: toolCallId,
|
||||
name: "edit",
|
||||
arguments: { path: "file.ts", op: "update", diff: "@@\n-old\n+new" },
|
||||
} satisfies ToolCall;
|
||||
|
||||
session.appendMessage(buildAssistantMessage([toolCall]));
|
||||
|
||||
const updated = await session.rewriteAssistantToolCallArgs(toolCallId, {
|
||||
path: "file.ts",
|
||||
op: "update",
|
||||
diff: "@@\n-old\n+newer",
|
||||
});
|
||||
|
||||
expect(updated).toBe(true);
|
||||
|
||||
const ctx = session.buildSessionContext();
|
||||
const assistant = ctx.messages.find(m => m.role === "assistant") as AssistantMessage;
|
||||
const updatedCall = assistant.content.find(b => b.type === "toolCall") as ToolCall;
|
||||
expect(updatedCall.arguments).toEqual({ path: "file.ts", op: "update", diff: "@@\n-old\n+newer" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user