decorated-pi 0.2.2 → 0.4.0
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 +82 -74
- package/extensions/file-times.ts +124 -0
- package/extensions/guidance.ts +5 -3
- package/extensions/index.ts +6 -2
- package/extensions/io.ts +587 -0
- package/extensions/lsp/client.ts +181 -428
- package/extensions/lsp/env.ts +45 -12
- package/extensions/lsp/format.ts +102 -237
- package/extensions/lsp/index.ts +8 -11
- package/extensions/lsp/manager.ts +249 -0
- package/extensions/lsp/prompt.ts +3 -42
- package/extensions/lsp/protocol.ts +219 -0
- package/extensions/lsp/servers.ts +80 -160
- package/extensions/lsp/tools.ts +175 -510
- package/extensions/lsp/types.ts +42 -0
- package/extensions/mcp/builtin.ts +126 -0
- package/extensions/mcp/client.ts +106 -0
- package/extensions/mcp/index.ts +123 -0
- package/extensions/{extend-model.ts → model-integration.ts} +127 -4
- package/extensions/patch.ts +842 -0
- package/extensions/providers/ark-coding.ts +2 -0
- package/extensions/safety/detect.ts +78 -707
- package/extensions/safety/entropy.ts +226 -0
- package/extensions/safety/index.ts +44 -97
- package/extensions/safety/patterns.ts +155 -0
- package/extensions/safety/types.ts +50 -0
- package/extensions/settings.ts +10 -0
- package/extensions/slash.ts +165 -9
- package/extensions/smart-at.ts +339 -111
- package/extensions/subdir-agents.ts +43 -13
- package/package.json +3 -4
- package/tsconfig.json +16 -0
- package/extensions/lsp/server-manager.ts +0 -309
- package/extensions/lsp/trust.ts +0 -45
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP type definitions — minimal set needed by this extension.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface LspPosition {
|
|
6
|
+
line: number;
|
|
7
|
+
character: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LspRange {
|
|
11
|
+
start: LspPosition;
|
|
12
|
+
end: LspPosition;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LspLocation {
|
|
16
|
+
uri: string;
|
|
17
|
+
range: LspRange;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LspDiagnostic {
|
|
21
|
+
range: LspRange;
|
|
22
|
+
severity?: number;
|
|
23
|
+
code?: unknown;
|
|
24
|
+
source?: string;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LspHover {
|
|
29
|
+
contents: unknown;
|
|
30
|
+
range?: LspRange;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LspDocumentSymbol {
|
|
34
|
+
name: string;
|
|
35
|
+
kind: number;
|
|
36
|
+
range: LspRange;
|
|
37
|
+
selectionRange?: LspRange;
|
|
38
|
+
containerName?: string;
|
|
39
|
+
detail?: string;
|
|
40
|
+
children?: LspDocumentSymbol[];
|
|
41
|
+
uri?: string;
|
|
42
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server configuration — builtin + global + project-level.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { loadConfig } from "../settings.js";
|
|
7
|
+
|
|
8
|
+
export interface McpServerConfig {
|
|
9
|
+
name: string;
|
|
10
|
+
url: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
source: "builtin" | "global" | "project";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Builtin servers — zero-config, always available unless overridden. */
|
|
17
|
+
export const BUILTIN_MCP_SERVERS: Omit<McpServerConfig, "source">[] = [
|
|
18
|
+
{
|
|
19
|
+
name: "context7",
|
|
20
|
+
url: "https://mcp.context7.com/mcp",
|
|
21
|
+
description: "Context7 documentation and code examples",
|
|
22
|
+
enabled: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "exa",
|
|
26
|
+
url: "https://mcp.exa.ai/mcp",
|
|
27
|
+
description: "Exa web search",
|
|
28
|
+
enabled: true,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// ── Project-level config discovery ─────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const PROJECT_CONFIG_PATHS = [
|
|
35
|
+
".pi/mcp.json",
|
|
36
|
+
".pi/.mcp.json",
|
|
37
|
+
".agents/mcp.json",
|
|
38
|
+
".agents/.mcp.json",
|
|
39
|
+
".claude/mcp.json",
|
|
40
|
+
".claude/.mcp.json",
|
|
41
|
+
"mcp.json",
|
|
42
|
+
".mcp.json",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function readMcpJson(filePath: string): Record<string, { url: string; enabled?: boolean }> | null {
|
|
46
|
+
try {
|
|
47
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
48
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
49
|
+
const servers = raw.mcpServers ?? raw["mcp-servers"];
|
|
50
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return null;
|
|
51
|
+
return servers as Record<string, { url: string; enabled?: boolean }>;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Load project-level MCP configs from cwd and its ancestor directories. */
|
|
58
|
+
export function loadProjectMcpConfigs(cwd: string): McpServerConfig[] {
|
|
59
|
+
const configs: McpServerConfig[] = [];
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
|
|
62
|
+
let current = path.resolve(cwd);
|
|
63
|
+
while (true) {
|
|
64
|
+
for (const relative of PROJECT_CONFIG_PATHS) {
|
|
65
|
+
const filePath = path.join(current, relative);
|
|
66
|
+
if (!fs.existsSync(filePath)) continue;
|
|
67
|
+
const servers = readMcpJson(filePath);
|
|
68
|
+
if (!servers) continue;
|
|
69
|
+
|
|
70
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
71
|
+
if (seen.has(name)) continue;
|
|
72
|
+
seen.add(name);
|
|
73
|
+
configs.push({
|
|
74
|
+
name,
|
|
75
|
+
url: entry.url,
|
|
76
|
+
enabled: entry.enabled !== false,
|
|
77
|
+
source: "project",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parent = path.dirname(current);
|
|
83
|
+
if (parent === current) break;
|
|
84
|
+
current = parent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return configs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Load global MCP configs from ~/.pi/agent/decorated-pi.json. */
|
|
91
|
+
export function loadGlobalMcpConfigs(): McpServerConfig[] {
|
|
92
|
+
const config = loadConfig();
|
|
93
|
+
if (!config.mcpServers) return [];
|
|
94
|
+
|
|
95
|
+
return Object.entries(config.mcpServers).map(([name, entry]) => ({
|
|
96
|
+
name,
|
|
97
|
+
url: entry.url,
|
|
98
|
+
enabled: entry.enabled !== false,
|
|
99
|
+
source: "global" as const,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Merge all MCP configs: builtin → global → project.
|
|
105
|
+
* Later sources override earlier ones for the same server name.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveMcpConfigs(cwd: string): McpServerConfig[] {
|
|
108
|
+
const byName = new Map<string, McpServerConfig>();
|
|
109
|
+
|
|
110
|
+
// Builtin (lowest priority)
|
|
111
|
+
for (const s of BUILTIN_MCP_SERVERS) {
|
|
112
|
+
byName.set(s.name, { ...s, source: "builtin" });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Global
|
|
116
|
+
for (const s of loadGlobalMcpConfigs()) {
|
|
117
|
+
byName.set(s.name, s);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Project (highest priority)
|
|
121
|
+
for (const s of loadProjectMcpConfigs(cwd)) {
|
|
122
|
+
byName.set(s.name, s);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [...byName.values()].filter((s) => s.enabled);
|
|
126
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
|
+
|
|
5
|
+
export interface McpToolSpec {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
inputSchema: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Per-server MCP client wrapper with fallback transport. */
|
|
12
|
+
export class McpConnection {
|
|
13
|
+
client: Client;
|
|
14
|
+
transport: StreamableHTTPClientTransport | SSEClientTransport | undefined;
|
|
15
|
+
tools: McpToolSpec[] = [];
|
|
16
|
+
private connected = false;
|
|
17
|
+
|
|
18
|
+
source: string = "unknown";
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
public readonly serverName: string,
|
|
22
|
+
public readonly url: string,
|
|
23
|
+
) {
|
|
24
|
+
this.client = new Client({
|
|
25
|
+
name: `decorated-pi-${serverName}`,
|
|
26
|
+
version: "0.3.0",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async connect(timeoutMs = 8000): Promise<void> {
|
|
31
|
+
const connectWithFallback = async (): Promise<void> => {
|
|
32
|
+
let lastErr: Error | undefined;
|
|
33
|
+
try {
|
|
34
|
+
const transport = new StreamableHTTPClientTransport(new URL(this.url));
|
|
35
|
+
await this.client.connect(transport);
|
|
36
|
+
this.transport = transport;
|
|
37
|
+
this.connected = true;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
40
|
+
try {
|
|
41
|
+
const transport = new SSEClientTransport(new URL(this.url));
|
|
42
|
+
await this.client.connect(transport);
|
|
43
|
+
this.transport = transport;
|
|
44
|
+
this.connected = true;
|
|
45
|
+
} catch (sseErr) {
|
|
46
|
+
const sseMessage = sseErr instanceof Error ? sseErr.message : String(sseErr);
|
|
47
|
+
throw new Error(
|
|
48
|
+
`MCP ${this.serverName}: StreamableHTTP failed (${lastErr.message}); SSE fallback also failed (${sseMessage})`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = (await this.client.listTools()) as unknown as {
|
|
54
|
+
tools: Array<{
|
|
55
|
+
name: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
inputSchema?: Record<string, unknown>;
|
|
58
|
+
}>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
this.tools = (result.tools ?? []).map((t) => ({
|
|
62
|
+
name: t.name,
|
|
63
|
+
description: t.description || "",
|
|
64
|
+
inputSchema: t.inputSchema || { type: "object", properties: {} },
|
|
65
|
+
}));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const timeout = new Promise<never>((_, reject) =>
|
|
69
|
+
setTimeout(() => reject(new Error(`MCP ${this.serverName}: connection timed out after ${timeoutMs}ms`)), timeoutMs),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await Promise.race([connectWithFallback(), timeout]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async callTool(name: string, args: Record<string, unknown>): Promise<string> {
|
|
76
|
+
if (!this.connected) {
|
|
77
|
+
throw new Error(`MCP ${this.serverName}: not connected`);
|
|
78
|
+
}
|
|
79
|
+
const result = (await this.client.callTool({
|
|
80
|
+
name,
|
|
81
|
+
arguments: args,
|
|
82
|
+
})) as unknown as {
|
|
83
|
+
content?: Array<{ type: string; text?: string }>;
|
|
84
|
+
isError?: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const text = (result.content ?? [])
|
|
88
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text" && typeof c.text === "string")
|
|
89
|
+
.map((c) => c.text)
|
|
90
|
+
.join("\n");
|
|
91
|
+
|
|
92
|
+
if (result.isError) {
|
|
93
|
+
throw new Error(text || `MCP tool "${name}" returned an error`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return text || "(empty result)";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async disconnect(): Promise<void> {
|
|
100
|
+
if (!this.connected) return;
|
|
101
|
+
this.connected = false;
|
|
102
|
+
try {
|
|
103
|
+
await this.client.close();
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { McpConnection } from "./client.js";
|
|
4
|
+
import { resolveMcpConfigs } from "./builtin.js";
|
|
5
|
+
|
|
6
|
+
export interface McpServerStatus {
|
|
7
|
+
name: string;
|
|
8
|
+
url: string;
|
|
9
|
+
source: string;
|
|
10
|
+
state: "connecting" | "connected" | "failed";
|
|
11
|
+
toolCount: number;
|
|
12
|
+
tools: Array<{ name: string; description: string }>;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let activeConnections: McpConnection[] = [];
|
|
17
|
+
let allServers = new Map<string, McpServerStatus>();
|
|
18
|
+
let connectPromise: Promise<void> | null = null;
|
|
19
|
+
|
|
20
|
+
export function setupMcp(pi: ExtensionAPI) {
|
|
21
|
+
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
22
|
+
void (async () => {
|
|
23
|
+
await teardownMcp();
|
|
24
|
+
|
|
25
|
+
const configs = resolveMcpConfigs(ctx.cwd);
|
|
26
|
+
if (configs.length === 0) return;
|
|
27
|
+
|
|
28
|
+
// Initialise every target server as "connecting"
|
|
29
|
+
allServers = new Map(
|
|
30
|
+
configs.map((s) => [
|
|
31
|
+
s.name,
|
|
32
|
+
{
|
|
33
|
+
name: s.name,
|
|
34
|
+
url: s.url,
|
|
35
|
+
source: s.source,
|
|
36
|
+
state: "connecting" as const,
|
|
37
|
+
toolCount: 0,
|
|
38
|
+
tools: [],
|
|
39
|
+
},
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
connectPromise = Promise.all(
|
|
44
|
+
configs.map(async (server) => {
|
|
45
|
+
const conn = new McpConnection(server.name, server.url);
|
|
46
|
+
conn.source = server.source;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await conn.connect();
|
|
50
|
+
activeConnections.push(conn);
|
|
51
|
+
|
|
52
|
+
for (const tool of conn.tools) {
|
|
53
|
+
const prefixedName = `${server.name}_${tool.name}`;
|
|
54
|
+
pi.registerTool({
|
|
55
|
+
name: prefixedName,
|
|
56
|
+
label: `MCP: ${tool.name}`,
|
|
57
|
+
description: tool.description,
|
|
58
|
+
promptSnippet: tool.description.slice(0, 120),
|
|
59
|
+
parameters: Type.Unsafe(tool.inputSchema as never),
|
|
60
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, _ctx2) => {
|
|
61
|
+
const text = await conn.callTool(
|
|
62
|
+
tool.name,
|
|
63
|
+
params as Record<string, unknown>,
|
|
64
|
+
);
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text" as const, text }],
|
|
67
|
+
isError: false,
|
|
68
|
+
details: { server: server.name, tool: tool.name },
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
allServers.set(server.name, {
|
|
75
|
+
name: server.name,
|
|
76
|
+
url: server.url,
|
|
77
|
+
source: server.source,
|
|
78
|
+
state: "connected",
|
|
79
|
+
toolCount: conn.tools.length,
|
|
80
|
+
tools: conn.tools.map((t) => ({ name: t.name, description: t.description })),
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
allServers.set(server.name, {
|
|
85
|
+
name: server.name,
|
|
86
|
+
url: server.url,
|
|
87
|
+
source: server.source,
|
|
88
|
+
state: "failed",
|
|
89
|
+
toolCount: 0,
|
|
90
|
+
tools: [],
|
|
91
|
+
error: msg,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
).then(() => undefined);
|
|
96
|
+
|
|
97
|
+
await connectPromise;
|
|
98
|
+
connectPromise = null;
|
|
99
|
+
})();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
pi.on("session_shutdown", () => {
|
|
103
|
+
void teardownMcp();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getMcpStatus(): McpServerStatus[] {
|
|
108
|
+
return [...allServers.values()];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function teardownMcp(): Promise<void> {
|
|
112
|
+
await Promise.all(
|
|
113
|
+
activeConnections.map(async (conn) => {
|
|
114
|
+
try {
|
|
115
|
+
await conn.disconnect();
|
|
116
|
+
} catch {
|
|
117
|
+
// Silently ignore disconnect errors.
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
activeConnections = [];
|
|
122
|
+
allServers = new Map();
|
|
123
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Model Integration — 模型集成
|
|
3
3
|
*
|
|
4
4
|
* 对外接口:
|
|
5
5
|
* analyzeImage(model, base64, mediaType, apiKey, headers) → Promise<string>
|
|
@@ -22,13 +22,14 @@ import {
|
|
|
22
22
|
} from "@earendil-works/pi-tui";
|
|
23
23
|
import OpenAI from "openai";
|
|
24
24
|
import { fileTypeFromFile } from "file-type";
|
|
25
|
-
import type
|
|
25
|
+
import { isContextOverflow, type Model } from "@earendil-works/pi-ai";
|
|
26
26
|
import {
|
|
27
27
|
loadConfig, saveConfig, parseModelKey, formatModelKey,
|
|
28
28
|
getImageModelKey, getCompactModelKey,
|
|
29
29
|
setImageModelKey, setCompactModelKey,
|
|
30
30
|
} from "./settings.js";
|
|
31
31
|
import * as fs from "node:fs";
|
|
32
|
+
import * as os from "node:os";
|
|
32
33
|
import { extname, resolve } from "node:path";
|
|
33
34
|
|
|
34
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -349,15 +350,134 @@ function getConfiguredCompactModel(registry: any): Model<any> | null {
|
|
|
349
350
|
return registry.find(parsed.provider, parsed.modelId) ?? null;
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
interface PiCompactionSettings {
|
|
354
|
+
enabled: boolean;
|
|
355
|
+
reserveTokens: number;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface AutoCompactionCandidate {
|
|
359
|
+
messages: any[];
|
|
360
|
+
usage: { tokens: number | null; contextWindow: number } | undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const DEFAULT_PI_COMPACTION_SETTINGS: PiCompactionSettings = {
|
|
364
|
+
enabled: true,
|
|
365
|
+
reserveTokens: 16_384,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
function readJsonObject(filePath: string): any | undefined {
|
|
369
|
+
try {
|
|
370
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
371
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
372
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
373
|
+
} catch {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function loadPiCompactionSettings(cwd: string): PiCompactionSettings {
|
|
379
|
+
const globalSettings = readJsonObject(resolve(os.homedir(), ".pi", "agent", "settings.json"));
|
|
380
|
+
const projectSettings = readJsonObject(resolve(cwd, ".pi", "settings.json"));
|
|
381
|
+
const merged = {
|
|
382
|
+
...DEFAULT_PI_COMPACTION_SETTINGS,
|
|
383
|
+
...(globalSettings?.compaction ?? {}),
|
|
384
|
+
...(projectSettings?.compaction ?? {}),
|
|
385
|
+
};
|
|
386
|
+
return {
|
|
387
|
+
enabled: merged.enabled !== false,
|
|
388
|
+
reserveTokens: typeof merged.reserveTokens === "number" ? merged.reserveTokens : DEFAULT_PI_COMPACTION_SETTINGS.reserveTokens,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getLastAssistantMessage(messages: any[]): any | undefined {
|
|
393
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
394
|
+
if (messages[i]?.role === "assistant") return messages[i];
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function shouldExpectAutoCompaction(
|
|
400
|
+
messages: any[],
|
|
401
|
+
usage: { tokens: number | null; contextWindow: number } | undefined,
|
|
402
|
+
settings: PiCompactionSettings,
|
|
403
|
+
): boolean {
|
|
404
|
+
if (!settings.enabled) return false;
|
|
405
|
+
|
|
406
|
+
const lastAssistant = getLastAssistantMessage(messages);
|
|
407
|
+
if (!lastAssistant) return false;
|
|
408
|
+
|
|
409
|
+
const contextWindow = usage?.contextWindow ?? 0;
|
|
410
|
+
if (contextWindow > 0 && isContextOverflow(lastAssistant, contextWindow)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!usage || usage.tokens === null) return false;
|
|
415
|
+
return usage.tokens > usage.contextWindow - settings.reserveTokens;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function shouldAutoResumeCompaction(
|
|
419
|
+
prePromptCompactionPending: boolean,
|
|
420
|
+
postAgentEndCandidate: AutoCompactionCandidate | null,
|
|
421
|
+
settings: PiCompactionSettings,
|
|
422
|
+
customInstructions?: string,
|
|
423
|
+
): boolean {
|
|
424
|
+
if (customInstructions !== undefined) return false;
|
|
425
|
+
if (prePromptCompactionPending) return true;
|
|
426
|
+
if (!postAgentEndCandidate) return false;
|
|
427
|
+
return shouldExpectAutoCompaction(postAgentEndCandidate.messages, postAgentEndCandidate.usage, settings);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export const __modelIntegrationTest = {
|
|
431
|
+
shouldExpectAutoCompaction,
|
|
432
|
+
shouldAutoResumeCompaction,
|
|
433
|
+
};
|
|
434
|
+
|
|
352
435
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
353
436
|
// 主入口(注册所有事件)
|
|
354
437
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
355
438
|
|
|
356
|
-
export function
|
|
439
|
+
export function setupModelIntegration(pi: ExtensionAPI) {
|
|
357
440
|
setupImageReadFallback(pi);
|
|
358
441
|
|
|
442
|
+
let prePromptCompactionPending = false;
|
|
443
|
+
let postAgentEndCandidate: AutoCompactionCandidate | null = null;
|
|
444
|
+
let currentCompactionIsAuto = false;
|
|
445
|
+
|
|
446
|
+
pi.on("input", () => {
|
|
447
|
+
prePromptCompactionPending = true;
|
|
448
|
+
postAgentEndCandidate = null;
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
pi.on("before_agent_start", () => {
|
|
452
|
+
prePromptCompactionPending = false;
|
|
453
|
+
postAgentEndCandidate = null;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
pi.on("agent_start", () => {
|
|
457
|
+
prePromptCompactionPending = false;
|
|
458
|
+
postAgentEndCandidate = null;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
pi.on("agent_end", (event, ctx) => {
|
|
462
|
+
prePromptCompactionPending = false;
|
|
463
|
+
postAgentEndCandidate = {
|
|
464
|
+
messages: event.messages,
|
|
465
|
+
usage: ctx.getContextUsage(),
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
|
|
359
469
|
// 自定义压缩模型
|
|
360
470
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
471
|
+
const compactionSettings = loadPiCompactionSettings(ctx.cwd);
|
|
472
|
+
currentCompactionIsAuto = shouldAutoResumeCompaction(
|
|
473
|
+
prePromptCompactionPending,
|
|
474
|
+
postAgentEndCandidate,
|
|
475
|
+
compactionSettings,
|
|
476
|
+
event.customInstructions,
|
|
477
|
+
);
|
|
478
|
+
prePromptCompactionPending = false;
|
|
479
|
+
postAgentEndCandidate = null;
|
|
480
|
+
|
|
361
481
|
const model = getConfiguredCompactModel(ctx.modelRegistry);
|
|
362
482
|
if (!model) return; // 没配 → Pi 默认
|
|
363
483
|
|
|
@@ -394,8 +514,11 @@ export function setupExtendModel(pi: ExtensionAPI) {
|
|
|
394
514
|
}
|
|
395
515
|
});
|
|
396
516
|
|
|
397
|
-
//
|
|
517
|
+
// 压缩后自动继续(仅自动压缩)
|
|
398
518
|
pi.on("session_compact", () => {
|
|
519
|
+
const shouldResume = currentCompactionIsAuto;
|
|
520
|
+
currentCompactionIsAuto = false;
|
|
521
|
+
if (!shouldResume) return;
|
|
399
522
|
pi.sendMessage({
|
|
400
523
|
customType: "auto_compact_resume",
|
|
401
524
|
content: "The context was just auto-compacted. Continue the current task based on the summary above. Do not repeat completed work. If unsure about progress, briefly summarize current state then continue.",
|