@dyyz1993/pi-coding-agent 0.74.24 → 0.74.25
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/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +307 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +384 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/package.json +6 -5
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import type { AgentToolResult } from "@dyyz1993/pi-agent-core";
|
|
5
|
+
import { Text } from "@dyyz1993/pi-tui";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@dyyz1993/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
export type ResourceType = "image" | "url" | "html" | "pdf" | "video" | "audio" | "markdown" | "text";
|
|
10
|
+
|
|
11
|
+
export interface PreviewDetails {
|
|
12
|
+
source: string;
|
|
13
|
+
absolutePath?: string;
|
|
14
|
+
resourceType: ResourceType;
|
|
15
|
+
mimeType?: string;
|
|
16
|
+
status: "ok" | "not_found" | "error";
|
|
17
|
+
size?: number;
|
|
18
|
+
title?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const EXT_TO_RESOURCE: Record<string, { resourceType: ResourceType; mimeType: string }> = {
|
|
23
|
+
".png": { resourceType: "image", mimeType: "image/png" },
|
|
24
|
+
".jpg": { resourceType: "image", mimeType: "image/jpeg" },
|
|
25
|
+
".jpeg": { resourceType: "image", mimeType: "image/jpeg" },
|
|
26
|
+
".gif": { resourceType: "image", mimeType: "image/gif" },
|
|
27
|
+
".webp": { resourceType: "image", mimeType: "image/webp" },
|
|
28
|
+
".svg": { resourceType: "image", mimeType: "image/svg+xml" },
|
|
29
|
+
".bmp": { resourceType: "image", mimeType: "image/bmp" },
|
|
30
|
+
".ico": { resourceType: "image", mimeType: "image/x-icon" },
|
|
31
|
+
".html": { resourceType: "html", mimeType: "text/html" },
|
|
32
|
+
".htm": { resourceType: "html", mimeType: "text/html" },
|
|
33
|
+
".pdf": { resourceType: "pdf", mimeType: "application/pdf" },
|
|
34
|
+
".mp4": { resourceType: "video", mimeType: "video/mp4" },
|
|
35
|
+
".webm": { resourceType: "video", mimeType: "video/webm" },
|
|
36
|
+
".ogg": { resourceType: "video", mimeType: "video/ogg" },
|
|
37
|
+
".mp3": { resourceType: "audio", mimeType: "audio/mpeg" },
|
|
38
|
+
".wav": { resourceType: "audio", mimeType: "audio/wav" },
|
|
39
|
+
".flac": { resourceType: "audio", mimeType: "audio/flac" },
|
|
40
|
+
".md": { resourceType: "markdown", mimeType: "text/markdown" },
|
|
41
|
+
".mdx": { resourceType: "markdown", mimeType: "text/mdx" },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const URL_PATTERN = /^https?:\/\//i;
|
|
45
|
+
|
|
46
|
+
function isLocalAddress(host: string): boolean {
|
|
47
|
+
if (!host) return false;
|
|
48
|
+
const lower = host.toLowerCase();
|
|
49
|
+
if (lower === "localhost" || lower === "127.0.0.1" || lower === "::1") return true;
|
|
50
|
+
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
|
|
51
|
+
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
|
|
52
|
+
if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkReachable(host: string, port: number, timeoutMs = 2000): Promise<boolean> {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const socket = createConnection({ host, port }, () => {
|
|
59
|
+
socket.destroy();
|
|
60
|
+
resolve(true);
|
|
61
|
+
});
|
|
62
|
+
socket.setTimeout(timeoutMs);
|
|
63
|
+
socket.on("timeout", () => {
|
|
64
|
+
socket.destroy();
|
|
65
|
+
resolve(false);
|
|
66
|
+
});
|
|
67
|
+
socket.on("error", () => {
|
|
68
|
+
socket.destroy();
|
|
69
|
+
resolve(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isUrl(source: string): boolean {
|
|
75
|
+
return URL_PATTERN.test(source);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectResource(
|
|
79
|
+
source: string,
|
|
80
|
+
cwd: string,
|
|
81
|
+
): {
|
|
82
|
+
resourceType: ResourceType;
|
|
83
|
+
mimeType?: string;
|
|
84
|
+
absolutePath?: string;
|
|
85
|
+
} {
|
|
86
|
+
if (isUrl(source)) {
|
|
87
|
+
return { resourceType: "url", absolutePath: source };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const absolutePath = isAbsolute(source) ? source : resolve(cwd, source);
|
|
91
|
+
const ext = extname(source).toLowerCase();
|
|
92
|
+
const mapped = EXT_TO_RESOURCE[ext];
|
|
93
|
+
if (mapped) {
|
|
94
|
+
return { resourceType: mapped.resourceType, mimeType: mapped.mimeType, absolutePath };
|
|
95
|
+
}
|
|
96
|
+
return { resourceType: "text", mimeType: "text/plain", absolutePath };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const PreviewParams = Type.Object({
|
|
100
|
+
source: Type.String({ description: "File path or URL to preview" }),
|
|
101
|
+
title: Type.Optional(Type.String({ description: "Optional display title for the card" })),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export default function (pi: ExtensionAPI) {
|
|
105
|
+
pi.registerTool({
|
|
106
|
+
name: "preview",
|
|
107
|
+
label: "Preview",
|
|
108
|
+
description:
|
|
109
|
+
"Preview a resource (image, URL, PDF, video, audio, markdown, etc.) as a card in the UI. Does not send file content to the LLM.",
|
|
110
|
+
promptSnippet: "Preview a file or URL as a card",
|
|
111
|
+
parameters: PreviewParams,
|
|
112
|
+
|
|
113
|
+
async execute(
|
|
114
|
+
_toolCallId: string,
|
|
115
|
+
params: { source: string; title?: string },
|
|
116
|
+
_signal?: AbortSignal,
|
|
117
|
+
_onUpdate?: unknown,
|
|
118
|
+
ctx?: ExtensionContext,
|
|
119
|
+
): Promise<AgentToolResult<PreviewDetails>> {
|
|
120
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
121
|
+
|
|
122
|
+
if (!params.source?.trim()) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: "Error: source is required" }],
|
|
125
|
+
details: { source: "", resourceType: "text", status: "error", error: "source required" },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { resourceType, mimeType, absolutePath } = detectResource(params.source, cwd);
|
|
130
|
+
|
|
131
|
+
if (resourceType === "url") {
|
|
132
|
+
// 对本地/LAN 地址做 TCP 可达性检测
|
|
133
|
+
if (params.source.startsWith("http://")) {
|
|
134
|
+
try {
|
|
135
|
+
const parsed = new URL(params.source);
|
|
136
|
+
if (isLocalAddress(parsed.hostname)) {
|
|
137
|
+
const port = parseInt(parsed.port || "80", 10);
|
|
138
|
+
const reachable = await checkReachable(parsed.hostname, port);
|
|
139
|
+
if (!reachable) {
|
|
140
|
+
const msg = `Preview 失败:${parsed.host} 未在局域网开放,服务可能只监听 127.0.0.1。请将服务绑定到 0.0.0.0 后重试。`;
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: msg }],
|
|
143
|
+
details: {
|
|
144
|
+
source: params.source,
|
|
145
|
+
absolutePath: params.source,
|
|
146
|
+
resourceType: "url",
|
|
147
|
+
status: "error",
|
|
148
|
+
title: params.title,
|
|
149
|
+
error: `${parsed.host} 未在局域网开放,可能只监听 127.0.0.1`,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// URL parse 失败,继续正常流程
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text", text: `Preview: ${params.source} (url)` }],
|
|
161
|
+
details: {
|
|
162
|
+
source: params.source,
|
|
163
|
+
absolutePath: params.source,
|
|
164
|
+
resourceType: "url",
|
|
165
|
+
status: "ok",
|
|
166
|
+
title: params.title,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!absolutePath || !existsSync(absolutePath)) {
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: "text", text: `Preview: ${params.source} not found` }],
|
|
174
|
+
details: {
|
|
175
|
+
source: params.source,
|
|
176
|
+
absolutePath,
|
|
177
|
+
resourceType,
|
|
178
|
+
status: "not_found",
|
|
179
|
+
title: params.title,
|
|
180
|
+
error: "file not found",
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const stat = statSync(absolutePath);
|
|
186
|
+
if (stat.isDirectory()) {
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: `Preview: ${params.source} is a directory` }],
|
|
189
|
+
details: {
|
|
190
|
+
source: params.source,
|
|
191
|
+
absolutePath,
|
|
192
|
+
resourceType,
|
|
193
|
+
status: "error",
|
|
194
|
+
title: params.title,
|
|
195
|
+
error: "is a directory",
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const sizeStr =
|
|
201
|
+
stat.size > 1024 * 1024
|
|
202
|
+
? `${(stat.size / (1024 * 1024)).toFixed(1)}MB`
|
|
203
|
+
: stat.size > 1024
|
|
204
|
+
? `${(stat.size / 1024).toFixed(1)}KB`
|
|
205
|
+
: `${stat.size}B`;
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: `Preview: ${params.source} (${resourceType}${mimeType ? `, ${mimeType}` : ""}, ${sizeStr})`,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
details: {
|
|
215
|
+
source: params.source,
|
|
216
|
+
absolutePath,
|
|
217
|
+
resourceType,
|
|
218
|
+
mimeType,
|
|
219
|
+
status: "ok",
|
|
220
|
+
size: stat.size,
|
|
221
|
+
title: params.title,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
renderCall(args, theme) {
|
|
227
|
+
let text = theme.fg("toolTitle", theme.bold("preview "));
|
|
228
|
+
if (args.title) text += theme.fg("dim", `"${args.title}" `);
|
|
229
|
+
text += theme.fg("muted", args.source ?? "");
|
|
230
|
+
return new Text(text, 0, 0);
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
renderResult(result, { expanded: _expanded }, theme) {
|
|
234
|
+
const details = result.details as PreviewDetails | undefined;
|
|
235
|
+
if (!details) {
|
|
236
|
+
const text = result.content[0];
|
|
237
|
+
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (details.error || details.status === "not_found") {
|
|
241
|
+
return new Text(theme.fg("error", `Preview: ${details.error ?? "error"}`), 0, 0);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const icon = resourceIcon(details.resourceType);
|
|
245
|
+
const label = details.title ?? details.source;
|
|
246
|
+
const sizeStr = details.size ? ` (${formatSize(details.size)})` : "";
|
|
247
|
+
|
|
248
|
+
return new Text(`${theme.fg("success", icon)} ${theme.fg("muted", `${label}${sizeStr}`)}`, 0, 0);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function resourceIcon(type: ResourceType): string {
|
|
254
|
+
switch (type) {
|
|
255
|
+
case "image":
|
|
256
|
+
return "🖼";
|
|
257
|
+
case "url":
|
|
258
|
+
return "🔗";
|
|
259
|
+
case "video":
|
|
260
|
+
return "🎬";
|
|
261
|
+
case "audio":
|
|
262
|
+
return "🎵";
|
|
263
|
+
case "pdf":
|
|
264
|
+
return "📄";
|
|
265
|
+
case "html":
|
|
266
|
+
return "🌐";
|
|
267
|
+
case "markdown":
|
|
268
|
+
return "📝";
|
|
269
|
+
default:
|
|
270
|
+
return "📋";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function formatSize(bytes: number): string {
|
|
275
|
+
if (bytes > 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
276
|
+
if (bytes > 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
277
|
+
return `${bytes}B`;
|
|
278
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Rules Engine: MatchHistory Staleness Scenarios & Reconciliation
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
Conditional rule matches are tracked in `matchHistory` (an array of `MatchRecord`).
|
|
6
|
+
The history can become **stale** when messages that triggered matches are removed or modified
|
|
7
|
+
without an explicit notification to the rules-engine plugin.
|
|
8
|
+
|
|
9
|
+
## Scenarios
|
|
10
|
+
|
|
11
|
+
### 1. Context Compaction
|
|
12
|
+
|
|
13
|
+
**What happens**: pi compacts old messages into a summary. ToolResult messages
|
|
14
|
+
that contained `details.rulesMatched` are replaced by a compact summary entry.
|
|
15
|
+
|
|
16
|
+
**Signal**: `session_compact` event fires. Plugin resets `cachedMatchHash` and clears `lastMessages`.
|
|
17
|
+
|
|
18
|
+
**Reconciliation**:
|
|
19
|
+
- `context` event fires on the next agent turn with the **post-compaction** message list
|
|
20
|
+
- `rebuildMatchHistory()` scans remaining messages — compacted-away matches disappear
|
|
21
|
+
- `turn_end` also checks for drift and sends corrected snapshot if needed
|
|
22
|
+
|
|
23
|
+
**Result**: Matched rules from compacted-away messages are automatically cleared.
|
|
24
|
+
|
|
25
|
+
### 2. Rollback / Abort Retry
|
|
26
|
+
|
|
27
|
+
**What happens**: User triggers abort_retry, which removes the last assistant turn
|
|
28
|
+
including its tool results. Previously matched rules are no longer in context.
|
|
29
|
+
|
|
30
|
+
**Signal**: No explicit event. The rolled-back messages are removed from the message list.
|
|
31
|
+
|
|
32
|
+
**Reconciliation**:
|
|
33
|
+
- `context` event fires on the next agent turn — rolled-back messages are absent
|
|
34
|
+
- `rebuildMatchHistory()` only finds matches in remaining messages
|
|
35
|
+
- `turn_end` catches this too
|
|
36
|
+
|
|
37
|
+
**Result**: Stale. Panel may show old matches until next agent turn or manual `requestSnapshot`.
|
|
38
|
+
|
|
39
|
+
### 3. Manual Message Deletion
|
|
40
|
+
|
|
41
|
+
**What happens**: User manually deletes a message (e.g., via UI). If the deleted message
|
|
42
|
+
was a toolResult with `details.rulesMatched`, the match is no longer valid.
|
|
43
|
+
|
|
44
|
+
**Signal**: No event at all.
|
|
45
|
+
|
|
46
|
+
**Reconciliation**:
|
|
47
|
+
- Same as rollback: `context` and `turn_end` eventually reconcile
|
|
48
|
+
- Frontend `requestSnapshot` (on panel open/refresh) triggers `rebuildMatchHistory(lastMessages)`
|
|
49
|
+
- But `lastMessages` may be stale if no `context` event fired since deletion
|
|
50
|
+
|
|
51
|
+
**Result**: Stale until next agent turn or `requestSnapshot` with fresh context.
|
|
52
|
+
|
|
53
|
+
### 4. Other Plugin Modifies/Hides Tool Results
|
|
54
|
+
|
|
55
|
+
**What happens**: Another plugin (e.g., todo-cleanup) hides or modifies a toolResult entry,
|
|
56
|
+
removing its `details.rulesMatched` field or replacing the message entirely.
|
|
57
|
+
|
|
58
|
+
**Signal**: No cross-plugin notification.
|
|
59
|
+
|
|
60
|
+
**Reconciliation**:
|
|
61
|
+
- `context` event sees the modified messages and rebuilds accordingly
|
|
62
|
+
- If the other plugin modifies messages between `context` events, stale data persists
|
|
63
|
+
|
|
64
|
+
**Result**: Same as deletion — eventually consistent after next `context`.
|
|
65
|
+
|
|
66
|
+
### 5. Session Fork / Switch
|
|
67
|
+
|
|
68
|
+
**What happens**: Session is forked or user switches to a different session. The forked
|
|
69
|
+
session has a copy of messages, but the rules-engine state is per-process.
|
|
70
|
+
|
|
71
|
+
**Signal**: `session_start` fires for the new session. Plugin sends fresh snapshot.
|
|
72
|
+
|
|
73
|
+
**Reconciliation**:
|
|
74
|
+
- New pi process → fresh state → `session_start` sends snapshot with empty history
|
|
75
|
+
- `context` event rebuilds from the forked messages
|
|
76
|
+
- Any matches present in the forked messages will be picked up
|
|
77
|
+
|
|
78
|
+
**Result**: Correct — matches from the fork are preserved, new matches start fresh.
|
|
79
|
+
|
|
80
|
+
## Reconciliation Mechanism Summary
|
|
81
|
+
|
|
82
|
+
| Mechanism | Trigger | Uses Fresh Data? | Cost |
|
|
83
|
+
|-----------|---------|------------------|------|
|
|
84
|
+
| `context` event | Every agent turn (before LLM call) | Yes (current messages) | Medium — scans all messages |
|
|
85
|
+
| `turn_end` event | End of every turn | Yes (cached `lastMessages`) | Low — hash comparison + conditional snapshot |
|
|
86
|
+
| `requestSnapshot` RPC | Frontend panel open/refresh | Yes (cached `lastMessages`) | Low — only when user requests |
|
|
87
|
+
| `session_compact` | After compaction | Clears cache → forces rebuild | Negligible |
|
|
88
|
+
|
|
89
|
+
## Latency Analysis
|
|
90
|
+
|
|
91
|
+
The `context` event fires via `transformContext` in the agent loop (`packages/agent/agent-loop.ts:145-146`),
|
|
92
|
+
called **before every LLM call**. This is the primary reconciliation point.
|
|
93
|
+
|
|
94
|
+
**Latency gap**: Between a message mutation (rollback, deletion, compaction) and the next LLM call,
|
|
95
|
+
the panel may show stale data. This is acceptable because:
|
|
96
|
+
|
|
97
|
+
1. Users rarely watch the rules panel during message mutations
|
|
98
|
+
2. Next agent turn reconciles immediately (sub-second)
|
|
99
|
+
3. `requestSnapshot` (panel open/refresh) uses `lastMessages` which is at most one turn stale
|
|
100
|
+
4. No event exists for message deletion in the pi extension API — polling would be the only alternative
|
|
101
|
+
|
|
102
|
+
**Why not poll?** Periodic `requestSnapshot` would require fresh messages (not cached), which means
|
|
103
|
+
calling into the agent state on a timer. The `context` event already provides this for free on every turn.
|
|
104
|
+
|
|
105
|
+
## Implementation Notes
|
|
106
|
+
|
|
107
|
+
- `rebuildMatchHistory()` is the single source of truth — shared by `context`, `turn_end`, and `requestSnapshot`
|
|
108
|
+
- `lastMessages` is cached from the most recent `context` event
|
|
109
|
+
- `cachedMatchHash` is used to avoid sending duplicate snapshots when nothing changed
|
|
110
|
+
- Frontend merges `matchHistory` on snapshot (doesn't wipe on empty) to survive between reconciliations
|
|
111
|
+
- All 5 scenarios are **eventually consistent** with at most one-turn latency — no additional mechanism needed
|