@oh-my-pi/snapcompact 16.0.6 → 16.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.0.8] - 2026-06-18
6
+
7
+ ### Added
8
+
9
+ - Added `<out>` block wrapping for tool results to improve document structure
10
+ - Rendered thinking process as italicized blocks above assistant text
11
+ - Displayed tool call intents as `//` comments in tool call headers
12
+ - Changed conversation role markers to standard Markdown headings
13
+
14
+ ### Changed
15
+
16
+ - Merged tool results into their corresponding tool call blocks
17
+ - Preserved prose formatting around tool calls to maintain conversation flow
18
+ - Hidden `_i` argument from tool call output when an intent is provided
19
+ - Optimized assistant turn output to group thinking and text blocks efficiently
20
+
21
+ ### Fixed
22
+
23
+ - Fixed improper splitting of assistant messages around useless tool calls
24
+
5
25
  ## [16.0.1] - 2026-06-15
6
26
 
7
27
  ### Added
@@ -409,6 +409,7 @@ export interface CompactionResult<T = CompactionDetails> {
409
409
  }
410
410
  export type ConvertToLlm<TMessage = Message> = (messages: TMessage[]) => Message[];
411
411
  export declare function createFileOps(): FileOperations;
412
+ export declare function isUrlSchemePath(path: string): boolean;
412
413
  export declare function computeFileLists(fileOps: FileOperations): CompactionDetails;
413
414
  export declare function upsertFileOperations(summary: string, readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
414
415
  /** Default per-tool-result character cap in serialized history. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/snapcompact",
4
- "version": "16.0.6",
4
+ "version": "16.0.8",
5
5
  "description": "Bitmap-frame context compression for vision-capable LLMs",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,9 +31,9 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-ai": "16.0.6",
35
- "@oh-my-pi/pi-natives": "16.0.6",
36
- "@oh-my-pi/pi-utils": "16.0.6"
34
+ "@oh-my-pi/pi-ai": "16.0.8",
35
+ "@oh-my-pi/pi-natives": "16.0.8",
36
+ "@oh-my-pi/pi-utils": "16.0.8"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/bun": "^1.3.14"
@@ -551,10 +551,17 @@ export function createFileOps(): FileOperations {
551
551
  edited: new Set(),
552
552
  };
553
553
  }
554
+ const URL_SCHEME_RE = /[a-z][a-z0-9+.-]*:\/\//i;
555
+
556
+ const HEADING_MARKER = " ¶";
557
+
558
+ export function isUrlSchemePath(path: string): boolean {
559
+ return URL_SCHEME_RE.test(path);
560
+ }
554
561
 
555
562
  export function computeFileLists(fileOps: FileOperations): CompactionDetails {
556
- const modified = new Set([...fileOps.edited, ...fileOps.written]);
557
- const readFiles = [...fileOps.read].filter(file => !modified.has(file)).sort();
563
+ const modified = new Set([...fileOps.edited, ...fileOps.written].filter(file => !isUrlSchemePath(file)));
564
+ const readFiles = [...fileOps.read].filter(file => !isUrlSchemePath(file) && !modified.has(file)).sort();
558
565
  const modifiedFiles = [...modified].sort();
559
566
  return { readFiles, modifiedFiles };
560
567
  }
@@ -679,15 +686,33 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
679
686
  const dimToolResults = options?.dimToolResults !== false;
680
687
  const parts: string[] = [];
681
688
 
682
- // Tool results flagged contextually useless (and their paired calls) carry
683
- // no information worth archiving — skip the whole pair.
689
+ // Tool results flagged contextually useless (and their paired calls) carry no
690
+ // information worth archiving — skip the whole pair. Surviving results are
691
+ // indexed by tool-call id so each merges into its originating `# Tool call`.
684
692
  const uselessCallIds = new Set<string>();
693
+ const resultTextByCallId = new Map<string, string>();
685
694
  for (const msg of messages) {
686
- if (msg.role === "toolResult" && msg.useless === true && msg.isError !== true) {
695
+ if (msg.role !== "toolResult") continue;
696
+ if (msg.useless === true && msg.isError !== true) {
687
697
  uselessCallIds.add(msg.toolCallId);
698
+ continue;
688
699
  }
700
+ const text = msg.content
701
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
702
+ .map(block => block.text)
703
+ .join("");
704
+ if (text) resultTextByCallId.set(msg.toolCallId, text);
689
705
  }
690
706
 
707
+ // Wrap a raw tool-result body in an `<out>` block, dimming only the body so
708
+ // the frame coloring keeps structure (headings, calls) loud.
709
+ const renderResultBlock = (rawText: string): string => {
710
+ const body = truncateForSummary(stripDimMarkers(rawText), toolResultMaxChars, headRatio);
711
+ return `<out>\n${dimToolResults ? `${DIM_ON}${body}${DIM_OFF}` : body}\n</out>`;
712
+ };
713
+
714
+ const mergedCallIds = new Set<string>();
715
+
691
716
  for (const msg of messages) {
692
717
  if (msg.role === "user") {
693
718
  const content =
@@ -697,22 +722,39 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
697
722
  .filter((content): content is { type: "text"; text: string } => content.type === "text")
698
723
  .map(content => content.text)
699
724
  .join("");
700
- if (content) parts.push(`[User]: ${stripDimMarkers(content)}`);
725
+ if (content) parts.push(`# User${HEADING_MARKER}\n${stripDimMarkers(content)}`);
701
726
  } else if (msg.role === "assistant") {
702
- const textParts: string[] = [];
703
- const thinkingParts: string[] = [];
704
- const toolCalls: string[] = [];
727
+ // Stream blocks in content order: buffer thinking/text, then flush a
728
+ // `# Assistant` block (thinking as italics above the text) right before
729
+ // each tool call, so text or thinking after a call stays after it.
730
+ let pendingThinking: string[] = [];
731
+ let pendingText: string[] = [];
732
+ const flushAssistant = () => {
733
+ const sections: string[] = [];
734
+ if (pendingThinking.length > 0) sections.push(`_${pendingThinking.join("\n")}_`);
735
+ if (pendingText.length > 0) sections.push(pendingText.join("\n"));
736
+ if (sections.length > 0) parts.push(`# Assistant${HEADING_MARKER}\n${sections.join("\n\n")}`);
737
+ pendingThinking = [];
738
+ pendingText = [];
739
+ };
705
740
 
706
741
  for (const block of msg.content) {
707
742
  if (block.type === "text") {
708
- textParts.push(stripDimMarkers(block.text));
743
+ pendingText.push(stripDimMarkers(block.text));
709
744
  } else if (block.type === "thinking") {
710
- thinkingParts.push(stripDimMarkers(block.thinking));
745
+ pendingThinking.push(stripDimMarkers(block.thinking));
711
746
  } else if (block.type === "toolCall") {
712
747
  if (uselessCallIds.has(block.id)) continue;
748
+ flushAssistant();
713
749
  const args = block.arguments as Record<string, unknown>;
750
+ // Prefer the harness-derived intent, else the raw `_i` arg; render it as
751
+ // a one-line `//comment` and drop `_i` from the args below.
752
+ const rawIntent =
753
+ typeof block.intent === "string" ? block.intent : typeof args._i === "string" ? args._i : "";
754
+ const intent = stripDimMarkers(rawIntent).replace(/\s+/g, " ").trim();
714
755
  const argsStr = truncateForSummary(
715
756
  Object.entries(args)
757
+ .filter(([key]) => key !== "_i")
716
758
  .map(
717
759
  ([key, value]) =>
718
760
  `${key}=${truncateForSummary(JSON.stringify(value) ?? "undefined", toolArgMaxChars, headRatio)}`,
@@ -721,30 +763,24 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
721
763
  toolCallMaxChars,
722
764
  headRatio,
723
765
  );
724
- toolCalls.push(`${block.name}(${argsStr})`);
766
+ const lines = [`# Tool call${HEADING_MARKER}`];
767
+ if (intent) lines.push(`//${intent}`);
768
+ lines.push(`${block.name}(${argsStr})`);
769
+ const resultText = resultTextByCallId.get(block.id);
770
+ if (resultText !== undefined) {
771
+ mergedCallIds.add(block.id);
772
+ lines.push(renderResultBlock(resultText));
773
+ }
774
+ parts.push(lines.join("\n"));
725
775
  }
726
776
  }
727
-
728
- if (thinkingParts.length > 0) {
729
- parts.push(`[Think]: ${thinkingParts.join("\n")}`);
730
- }
731
- if (textParts.length > 0) {
732
- parts.push(`[Assistant]: ${textParts.join("\n")}`);
733
- }
734
- if (toolCalls.length > 0) {
735
- parts.push(`[Tool Call]: ${toolCalls.join("; ")}`);
736
- }
777
+ flushAssistant();
737
778
  } else if (msg.role === "toolResult") {
738
- if (uselessCallIds.has(msg.toolCallId)) continue;
739
- const content = msg.content
740
- .filter((block): block is { type: "text"; text: string } => block.type === "text")
741
- .map(block => block.text)
742
- .join("");
743
- if (content) {
744
- // Args above are JSON-escaped, so only raw result text can carry toggles.
745
- const body = truncateForSummary(stripDimMarkers(content), toolResultMaxChars, headRatio);
746
- parts.push(dimToolResults ? `[Tool Result]: ${DIM_ON}${body}${DIM_OFF}` : `[Tool Result]: ${body}`);
747
- }
779
+ // Paired results already merged into their `# Tool call` block above;
780
+ // only orphans (call archived outside this window) render standalone.
781
+ if (uselessCallIds.has(msg.toolCallId) || mergedCallIds.has(msg.toolCallId)) continue;
782
+ const resultText = resultTextByCallId.get(msg.toolCallId);
783
+ if (resultText !== undefined) parts.push(`# Tool call${HEADING_MARKER}\n${renderResultBlock(resultText)}`);
748
784
  }
749
785
  }
750
786