@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 +2 -0
- package/package.json +1 -1
- package/src/core/content.ts +22 -0
- package/src/core/fabric-trace.ts +118 -0
- package/src/core/normalize.ts +20 -1
- package/src/core/render-entries.ts +14 -1
- package/src/core/search-entries.ts +17 -2
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
package/src/core/content.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/core/normalize.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|