@monotykamary/pi-vcc 0.8.0 → 0.8.2

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/README.md CHANGED
@@ -248,6 +248,8 @@ When `PI_FABRIC_COMPACTION_ENGINE=fabric`, pi-vcc defers non-explicit summary co
248
248
 
249
249
  The precedence is: explicit `/pi-vcc` sentinel > pi-fabric engine > pi-vcc default override.
250
250
 
251
+ For explicit `/pi-vcc` compactions, pi-vcc also expands the durable execution trace stored on each `fabric_exec` result. Nested `pi.*`, MCP, extension, agent, and other Fabric provider calls participate in file tracking, error extraction, the brief transcript, and `vcc_recall` search just like top-level tool calls. Current trace details and legacy Fabric audit details are both supported.
252
+
251
253
  ### How compaction works
252
254
 
253
255
  Pi splits the conversation at the **last user message**. Everything after — the **kept tail** — stays intact and untouched. pi-vcc only summarizes the older portion before that cut point.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-vcc",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Algorithmic conversation compactor for pi - transcript-preserving structured summaries, no LLM calls",
5
5
  "main": "index.ts",
6
6
  "keywords": [
@@ -59,6 +59,28 @@ const thinkingParts = (content: Message["content"]): string[] => {
59
59
  export const thinkingOf = (content: Message["content"]): string =>
60
60
  thinkingParts(content).join("\n");
61
61
 
62
+ const toolCallArgText = (args: Record<string, unknown>): string => {
63
+ const vals: string[] = [];
64
+ for (const v of Object.values(args ?? {})) {
65
+ if (typeof v === "string" && v.length > 0) vals.push(v);
66
+ }
67
+ return vals.join("\n");
68
+ };
69
+
70
+ const toolCallParts = (content: Message["content"]): string[] => {
71
+ if (!content || typeof content === "string") return [];
72
+ return content
73
+ .filter((part) => part.type === "toolCall")
74
+ .map((part) => toolCallArgText(part.arguments));
75
+ };
76
+
77
+ /** Extract all string-valued arguments from toolCall content parts.
78
+ * Lets vcc_recall match against tool invocations — e.g. a bash
79
+ * toolCall's `arguments.command`, a grep's `pattern`, an edit's
80
+ * `oldText`/`newText`. Non-string args are skipped. */
81
+ export const toolCallsOf = (content: Message["content"]): string =>
82
+ toolCallParts(content).filter(Boolean).join("\n");
83
+
62
84
  /** Extract a snippet of ~`radius` chars around the first match of `term` in `text`. */
63
85
  const snippet = (text: string, term: string, radius = 60): string | null => {
64
86
  const idx = text.toLowerCase().indexOf(term.toLowerCase());
@@ -0,0 +1,118 @@
1
+ type FabricOperationOutcome = "succeeded" | "failed" | "aborted" | "timed_out";
2
+
3
+ export interface FabricOperation {
4
+ ref: string;
5
+ name: string;
6
+ args: Record<string, unknown>;
7
+ outcome: FabricOperationOutcome;
8
+ error?: string;
9
+ result?: unknown;
10
+ }
11
+
12
+ const OUTCOMES = new Set<FabricOperationOutcome>([
13
+ "succeeded",
14
+ "failed",
15
+ "aborted",
16
+ "timed_out",
17
+ ]);
18
+
19
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
20
+ typeof value === "object" && value !== null && !Array.isArray(value);
21
+
22
+ const identityOf = (ref: string, provider?: unknown, action?: unknown) => {
23
+ const separator = ref.indexOf(".");
24
+ const lexicalProvider = separator > 0 ? ref.slice(0, separator) : undefined;
25
+ const lexicalAction = separator > 0 && separator < ref.length - 1
26
+ ? ref.slice(separator + 1)
27
+ : undefined;
28
+ const resolvedProvider = typeof provider === "string" ? provider : lexicalProvider;
29
+ const resolvedAction = typeof action === "string" ? action : lexicalAction;
30
+ return {
31
+ name: resolvedProvider === "pi" && resolvedAction
32
+ ? resolvedAction
33
+ : resolvedAction ?? ref,
34
+ };
35
+ };
36
+
37
+ const operationOf = (value: unknown): FabricOperation | undefined => {
38
+ if (!isRecord(value) || typeof value.ref !== "string" || !isRecord(value.args)) {
39
+ return undefined;
40
+ }
41
+ if (!OUTCOMES.has(value.outcome as FabricOperationOutcome)) return undefined;
42
+ if (value.error !== undefined && typeof value.error !== "string") return undefined;
43
+ return {
44
+ ref: value.ref,
45
+ name: identityOf(value.ref, value.provider, value.action).name,
46
+ args: value.args,
47
+ outcome: value.outcome as FabricOperationOutcome,
48
+ ...(typeof value.error === "string" ? { error: value.error } : {}),
49
+ ...(Object.prototype.hasOwnProperty.call(value, "result") ? { result: value.result } : {}),
50
+ };
51
+ };
52
+
53
+ const legacyOperationOf = (value: unknown): FabricOperation | undefined => {
54
+ if (!isRecord(value) || typeof value.ref !== "string" || !isRecord(value.args)) {
55
+ return undefined;
56
+ }
57
+ if (typeof value.success !== "boolean") return undefined;
58
+ if (value.error !== undefined && typeof value.error !== "string") return undefined;
59
+ return {
60
+ ref: value.ref,
61
+ name: identityOf(value.ref).name,
62
+ args: value.args,
63
+ outcome: value.success ? "succeeded" : "failed",
64
+ ...(typeof value.error === "string" ? { error: value.error } : {}),
65
+ ...(Object.prototype.hasOwnProperty.call(value, "result") ? { result: value.result } : {}),
66
+ };
67
+ };
68
+
69
+ /** Read the durable nested-operation projection stored by pi-fabric on a fabric_exec result. */
70
+ export const fabricOperationsOf = (details: unknown): FabricOperation[] => {
71
+ if (!isRecord(details)) return [];
72
+ if (isRecord(details.trace)) {
73
+ const trace = details.trace;
74
+ if (trace.kind !== "pi-fabric.execution" || trace.version !== 1 || !Array.isArray(trace.operations)) {
75
+ return [];
76
+ }
77
+ const operations: FabricOperation[] = [];
78
+ for (const value of trace.operations) {
79
+ const operation = operationOf(value);
80
+ if (!operation) return [];
81
+ operations.push(operation);
82
+ }
83
+ return operations;
84
+ }
85
+ if (!Array.isArray(details.audits)) return [];
86
+ const operations: FabricOperation[] = [];
87
+ for (const value of details.audits) {
88
+ const operation = legacyOperationOf(value);
89
+ if (!operation) return [];
90
+ operations.push(operation);
91
+ }
92
+ return operations;
93
+ };
94
+
95
+ export const fabricArgsText = (operation: FabricOperation): string => {
96
+ try {
97
+ return JSON.stringify(operation.args);
98
+ } catch {
99
+ return Object.keys(operation.args).join(" ");
100
+ }
101
+ };
102
+
103
+ export const fabricResultText = (operation: FabricOperation): string => {
104
+ if (operation.error) return operation.error;
105
+ const value = operation.result;
106
+ if (typeof value === "string") return value;
107
+ if (isRecord(value)) {
108
+ for (const key of ["output", "text", "content"]) {
109
+ if (typeof value[key] === "string") return value[key] as string;
110
+ }
111
+ }
112
+ if (value === undefined) return "";
113
+ try {
114
+ return JSON.stringify(value);
115
+ } catch {
116
+ return String(value);
117
+ }
118
+ };
@@ -2,6 +2,7 @@ import type { Message } from "@earendil-works/pi-ai";
2
2
  import type { NormalizedBlock } from "../types";
3
3
  import { textOf } from "./content";
4
4
  import { sanitize } from "./sanitize";
5
+ import { fabricOperationsOf, fabricResultText } from "./fabric-trace";
5
6
 
6
7
  const normalizeOne = (msg: Message, msgIndex: number): NormalizedBlock[] => {
7
8
  if (msg.role === "user") {
@@ -26,13 +27,31 @@ const normalizeOne = (msg: Message, msgIndex: number): NormalizedBlock[] => {
26
27
  }
27
28
 
28
29
  if (msg.role === "toolResult") {
29
- return [{
30
+ const blocks: NormalizedBlock[] = [{
30
31
  kind: "tool_result",
31
32
  name: msg.toolName,
32
33
  text: sanitize(textOf(msg.content)),
33
34
  isError: msg.isError,
34
35
  sourceIndex: msgIndex,
35
36
  }];
37
+ if (msg.toolName === "fabric_exec") {
38
+ for (const operation of fabricOperationsOf((msg as any).details)) {
39
+ blocks.push({
40
+ kind: "tool_call",
41
+ name: operation.name,
42
+ args: operation.args,
43
+ sourceIndex: msgIndex,
44
+ });
45
+ blocks.push({
46
+ kind: "tool_result",
47
+ name: operation.name,
48
+ text: sanitize(fabricResultText(operation)),
49
+ isError: operation.outcome !== "succeeded",
50
+ sourceIndex: msgIndex,
51
+ });
52
+ }
53
+ }
54
+ return blocks;
36
55
  }
37
56
 
38
57
  if (msg.role === "assistant") {
@@ -2,6 +2,7 @@ import type { Message } from "@earendil-works/pi-ai";
2
2
  import { clip, textOf, thinkingOf } from "./content";
3
3
  import { summarizeToolArgs } from "./tool-args";
4
4
  import { extractPath } from "./tool-args";
5
+ import { fabricOperationsOf } from "./fabric-trace";
5
6
 
6
7
  export interface RenderedEntry {
7
8
  index: number;
@@ -33,9 +34,21 @@ export const renderMessage = (msg: Message, index: number, full = false): Render
33
34
  if (msg.role === "toolResult") {
34
35
  const prefix = msg.isError ? "ERROR " : "";
35
36
  const text = full ? textOf(msg.content) : clip(textOf(msg.content), 200);
37
+ const nested = msg.toolName === "fabric_exec"
38
+ ? fabricOperationsOf((msg as any).details)
39
+ : [];
40
+ const nestedSummary = nested
41
+ .slice(-20)
42
+ .map((operation) => `${operation.name}(${summarizeToolArgs(operation.args)})`)
43
+ .join(", ");
44
+ const files = [...new Set(nested
45
+ .map((operation) => extractPath(operation.args))
46
+ .filter((path): path is string => path !== null))]
47
+ .slice(0, 50);
36
48
  return {
37
49
  index, role: "tool_result",
38
- summary: `${prefix}[${msg.toolName}] ${text}`,
50
+ summary: `${prefix}[${msg.toolName}] ${text}${nestedSummary ? `\n${nestedSummary}` : ""}`,
51
+ ...(files.length > 0 && { files }),
39
52
  };
40
53
  }
41
54
  // bashExecution has command+output instead of content
@@ -1,6 +1,7 @@
1
1
  import type { Message } from "@earendil-works/pi-ai";
2
2
  import type { RenderedEntry } from "./render-entries";
3
- import { textOf, thinkingOf } from "./content";
3
+ import { textOf, thinkingOf, toolCallsOf } from "./content";
4
+ import { fabricArgsText, fabricOperationsOf, fabricResultText } from "./fabric-trace";
4
5
 
5
6
  export interface SearchHit extends RenderedEntry {
6
7
  /** Context snippet around the first matched term (only when query provided) */
@@ -270,10 +271,24 @@ const fullText = (msg: Message): string => {
270
271
  if ((msg as any).role === "bashExecution") {
271
272
  return `${(msg as any).command ?? ""} ${(msg as any).output ?? ""}`;
272
273
  }
273
- // Include thinking content so recall can match against model reasoning
274
+ // Include thinking + toolCall arguments so recall can match against
275
+ // model reasoning and tool invocations (e.g. a bash toolCall's command).
274
276
  let text = textOf(msg.content);
277
+ if (msg.role === "toolResult" && msg.toolName === "fabric_exec") {
278
+ const nested = fabricOperationsOf((msg as any).details)
279
+ .flatMap((operation) => [
280
+ operation.ref,
281
+ fabricArgsText(operation),
282
+ fabricResultText(operation),
283
+ ])
284
+ .filter(Boolean)
285
+ .join("\n");
286
+ if (nested) text += "\n" + nested;
287
+ }
275
288
  const thinking = thinkingOf(msg.content);
276
289
  if (thinking) text = thinking + "\n" + text;
290
+ const toolArgs = toolCallsOf(msg.content);
291
+ if (toolArgs) text = toolArgs + "\n" + text;
277
292
  return text;
278
293
  };
279
294