@docyrus/docyrus 0.0.22 → 0.0.23
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 +12 -0
- package/main.js +234 -11
- package/main.js.map +4 -4
- package/package.json +6 -4
- package/resources/pi-agent/assets/docyrus-logo.svg +16 -0
- package/resources/pi-agent/extensions/architect.ts +771 -0
- package/resources/pi-agent/extensions/notify.ts +57 -55
- package/resources/pi-agent/extensions/pi-custom-compaction/CHANGELOG.md +27 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/LICENSE +21 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/README.md +244 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/VENDORED_FROM.md +6 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/banner.png +0 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/commands/register-commands.ts +63 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/events/register-events.ts +229 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/index.ts +10 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/package.json +57 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/paths.ts +13 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/policy/config.ts +32 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/policy/merge.ts +67 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/policy/parse.ts +354 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/policy/types.ts +131 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/runtime/model-resolution.ts +77 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/runtime/pure.ts +56 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/runtime/session-state.ts +244 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/summary/generate.ts +184 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/summary/template.ts +124 -0
- package/server-loader.js +3841 -0
- package/server-loader.js.map +7 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Loader } from "@mariozechner/pi-tui";
|
|
3
|
+
import { mergePolicy } from "../policy/merge.js";
|
|
4
|
+
import { readProjectPolicyPatch } from "../policy/config.js";
|
|
5
|
+
import { DEFAULT_POLICY, type ICompactionPolicy } from "../policy/types.js";
|
|
6
|
+
|
|
7
|
+
const STATUS_KEY = "compact-policy";
|
|
8
|
+
const WATCHDOG_MS = 120_000;
|
|
9
|
+
|
|
10
|
+
interface IInFlightState {
|
|
11
|
+
active: boolean;
|
|
12
|
+
source: string;
|
|
13
|
+
timerId: NodeJS.Timeout | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IRuntimeServices {
|
|
17
|
+
clearInFlight: () => void;
|
|
18
|
+
setInFlight: (source: string) => void;
|
|
19
|
+
isInFlight: () => boolean;
|
|
20
|
+
getLastProactiveAtMs: () => number | undefined;
|
|
21
|
+
setLastProactiveAtMs: (value: number | undefined) => void;
|
|
22
|
+
setActiveProfileName: (name: string | undefined) => void;
|
|
23
|
+
markPostCompact: () => void;
|
|
24
|
+
notify: (
|
|
25
|
+
ctx: ExtensionContext,
|
|
26
|
+
policy: ICompactionPolicy,
|
|
27
|
+
level: "info" | "warning" | "error",
|
|
28
|
+
message: string,
|
|
29
|
+
options?: { critical?: boolean; dedupeKey?: string },
|
|
30
|
+
) => boolean;
|
|
31
|
+
updateStatus: (ctx: ExtensionContext, policy: ICompactionPolicy) => void;
|
|
32
|
+
clearSessionScopedState: (ctx: ExtensionContext) => void;
|
|
33
|
+
loadEffectivePolicy: (
|
|
34
|
+
ctx: ExtensionContext,
|
|
35
|
+
options?: {
|
|
36
|
+
warnOnInvalidConfig?: boolean;
|
|
37
|
+
},
|
|
38
|
+
) => ICompactionPolicy;
|
|
39
|
+
triggerCompaction: (
|
|
40
|
+
ctx: ExtensionContext,
|
|
41
|
+
policy: ICompactionPolicy,
|
|
42
|
+
source: string,
|
|
43
|
+
customInstructions?: string,
|
|
44
|
+
) => boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createRuntimeServices(): IRuntimeServices {
|
|
48
|
+
const inFlight: IInFlightState = { active: false, source: "", timerId: undefined };
|
|
49
|
+
let lastProactiveAtMs: number | undefined;
|
|
50
|
+
let activeProfileName: string | undefined;
|
|
51
|
+
let configWarningLatched = false;
|
|
52
|
+
const warnedReasons = new Set<string>();
|
|
53
|
+
let widgetCtx: ExtensionContext | undefined;
|
|
54
|
+
let postCompact = false;
|
|
55
|
+
const WIDGET_KEY = "compact-loader";
|
|
56
|
+
|
|
57
|
+
function showCompactionWidget(ctx: ExtensionContext) {
|
|
58
|
+
hideCompactionWidget();
|
|
59
|
+
ctx.ui.setWidget(WIDGET_KEY, (tui, theme) => {
|
|
60
|
+
const loader = new Loader(
|
|
61
|
+
tui,
|
|
62
|
+
(s) => theme.fg("accent", s),
|
|
63
|
+
(t) => theme.fg("muted", t),
|
|
64
|
+
"Compacting…",
|
|
65
|
+
);
|
|
66
|
+
return Object.assign(loader, { dispose: () => loader.stop() });
|
|
67
|
+
});
|
|
68
|
+
widgetCtx = ctx;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hideCompactionWidget() {
|
|
72
|
+
if (!widgetCtx) {return;}
|
|
73
|
+
widgetCtx.ui.setWidget(WIDGET_KEY, undefined);
|
|
74
|
+
widgetCtx = undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function clearInFlight() {
|
|
78
|
+
if (inFlight.timerId) {
|
|
79
|
+
clearTimeout(inFlight.timerId);
|
|
80
|
+
inFlight.timerId = undefined;
|
|
81
|
+
}
|
|
82
|
+
inFlight.active = false;
|
|
83
|
+
inFlight.source = "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setInFlight(source: string) {
|
|
87
|
+
clearInFlight();
|
|
88
|
+
inFlight.active = true;
|
|
89
|
+
inFlight.source = source;
|
|
90
|
+
inFlight.timerId = setTimeout(() => {
|
|
91
|
+
clearInFlight();
|
|
92
|
+
hideCompactionWidget();
|
|
93
|
+
}, WATCHDOG_MS);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function notify(
|
|
97
|
+
ctx: ExtensionContext,
|
|
98
|
+
policy: ICompactionPolicy,
|
|
99
|
+
level: "info" | "warning" | "error",
|
|
100
|
+
message: string,
|
|
101
|
+
options?: { critical?: boolean; dedupeKey?: string },
|
|
102
|
+
): boolean {
|
|
103
|
+
if (policy.ui.quiet && !options?.critical && level !== "error") {return false;}
|
|
104
|
+
if (options?.dedupeKey) {
|
|
105
|
+
if (warnedReasons.has(options.dedupeKey)) {return false;}
|
|
106
|
+
warnedReasons.add(options.dedupeKey);
|
|
107
|
+
}
|
|
108
|
+
ctx.ui.notify(message, level);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function updateStatus(ctx: ExtensionContext, policy: ICompactionPolicy) {
|
|
113
|
+
if (!policy.enabled || !policy.ui.showStatus) {
|
|
114
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const base = policy.ui.name;
|
|
119
|
+
const prefix = activeProfileName ? `${base}: ${activeProfileName}` : base;
|
|
120
|
+
|
|
121
|
+
if (inFlight.active) {
|
|
122
|
+
ctx.ui.setStatus(STATUS_KEY, `${prefix} · compacting…`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const usage = ctx.getContextUsage();
|
|
127
|
+
|
|
128
|
+
if (!usage || usage.tokens === null) {
|
|
129
|
+
ctx.ui.setStatus(STATUS_KEY, postCompact ? prefix : `${prefix} · ?`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
postCompact = false;
|
|
133
|
+
|
|
134
|
+
const { maxTokens } = policy.trigger;
|
|
135
|
+
const limit = maxTokens && maxTokens > 0 && maxTokens < usage.contextWindow
|
|
136
|
+
? maxTokens
|
|
137
|
+
: usage.contextWindow;
|
|
138
|
+
const pct = limit > 0 ? (usage.tokens / limit) * 100 : 0;
|
|
139
|
+
|
|
140
|
+
ctx.ui.setStatus(
|
|
141
|
+
STATUS_KEY,
|
|
142
|
+
policy.ui.minimalStatus
|
|
143
|
+
? `${prefix} · ${pct.toFixed(0)}%`
|
|
144
|
+
: `${prefix} · ${pct.toFixed(1)}% (${usage.tokens}/${limit})`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function loadEffectivePolicy(
|
|
149
|
+
ctx: ExtensionContext,
|
|
150
|
+
options?: {
|
|
151
|
+
warnOnInvalidConfig?: boolean;
|
|
152
|
+
},
|
|
153
|
+
): ICompactionPolicy {
|
|
154
|
+
const result = readProjectPolicyPatch(ctx.cwd);
|
|
155
|
+
if (result.ok) {
|
|
156
|
+
configWarningLatched = false;
|
|
157
|
+
return mergePolicy(DEFAULT_POLICY, result.value);
|
|
158
|
+
}
|
|
159
|
+
if (options?.warnOnInvalidConfig !== false && !configWarningLatched) {
|
|
160
|
+
const emitted = notify(
|
|
161
|
+
ctx,
|
|
162
|
+
DEFAULT_POLICY,
|
|
163
|
+
"warning",
|
|
164
|
+
`${result.error}. Using built-in defaults.`,
|
|
165
|
+
);
|
|
166
|
+
if (emitted) {configWarningLatched = true;}
|
|
167
|
+
}
|
|
168
|
+
return DEFAULT_POLICY;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function clearSessionScopedState(ctx: ExtensionContext) {
|
|
172
|
+
clearInFlight();
|
|
173
|
+
hideCompactionWidget();
|
|
174
|
+
lastProactiveAtMs = undefined;
|
|
175
|
+
activeProfileName = undefined;
|
|
176
|
+
postCompact = false;
|
|
177
|
+
configWarningLatched = false;
|
|
178
|
+
warnedReasons.clear();
|
|
179
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function triggerCompaction(
|
|
183
|
+
ctx: ExtensionContext,
|
|
184
|
+
policy: ICompactionPolicy,
|
|
185
|
+
source: string,
|
|
186
|
+
customInstructions?: string,
|
|
187
|
+
): boolean {
|
|
188
|
+
if (inFlight.active) {
|
|
189
|
+
notify(ctx, policy, "warning", `Compaction already in progress (${inFlight.source}).`);
|
|
190
|
+
updateStatus(ctx, policy);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setInFlight(source);
|
|
195
|
+
try {
|
|
196
|
+
showCompactionWidget(ctx);
|
|
197
|
+
ctx.compact({
|
|
198
|
+
customInstructions,
|
|
199
|
+
onComplete: () => {
|
|
200
|
+
clearInFlight();
|
|
201
|
+
hideCompactionWidget();
|
|
202
|
+
updateStatus(ctx, policy);
|
|
203
|
+
},
|
|
204
|
+
onError: (error) => {
|
|
205
|
+
clearInFlight();
|
|
206
|
+
hideCompactionWidget();
|
|
207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
+
notify(ctx, policy, "error", `Compaction failed: ${message}`, { critical: true });
|
|
209
|
+
updateStatus(ctx, policy);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
clearInFlight();
|
|
214
|
+
hideCompactionWidget();
|
|
215
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
216
|
+
notify(ctx, policy, "error", `Compaction failed: ${message}`, { critical: true });
|
|
217
|
+
updateStatus(ctx, policy);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
updateStatus(ctx, policy);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
clearInFlight,
|
|
226
|
+
setInFlight,
|
|
227
|
+
isInFlight: () => inFlight.active,
|
|
228
|
+
getLastProactiveAtMs: () => lastProactiveAtMs,
|
|
229
|
+
setLastProactiveAtMs: (value: number | undefined) => {
|
|
230
|
+
lastProactiveAtMs = value;
|
|
231
|
+
},
|
|
232
|
+
setActiveProfileName: (name: string | undefined) => {
|
|
233
|
+
activeProfileName = name;
|
|
234
|
+
},
|
|
235
|
+
markPostCompact: () => {
|
|
236
|
+
postCompact = true;
|
|
237
|
+
},
|
|
238
|
+
notify,
|
|
239
|
+
updateStatus,
|
|
240
|
+
clearSessionScopedState,
|
|
241
|
+
loadEffectivePolicy,
|
|
242
|
+
triggerCompaction,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import { completeSimple } from "@mariozechner/pi-ai";
|
|
3
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
4
|
+
import {
|
|
5
|
+
convertToLlm,
|
|
6
|
+
serializeConversation,
|
|
7
|
+
type SessionBeforeCompactEvent,
|
|
8
|
+
} from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { ICompactionDetails, ISummaryThinkingLevel } from "../policy/types.js";
|
|
10
|
+
|
|
11
|
+
const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
|
|
12
|
+
|
|
13
|
+
Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;
|
|
14
|
+
|
|
15
|
+
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
|
|
16
|
+
|
|
17
|
+
Summarize the prefix to provide context for the retained suffix:
|
|
18
|
+
|
|
19
|
+
## Original Request
|
|
20
|
+
[What did the user ask for in this turn?]
|
|
21
|
+
|
|
22
|
+
## Early Progress
|
|
23
|
+
- [Key decisions and work done in the prefix]
|
|
24
|
+
|
|
25
|
+
## Context for Suffix
|
|
26
|
+
- [Information needed to understand the kept suffix]
|
|
27
|
+
|
|
28
|
+
Be concise. Focus on what's needed to understand the retained recent work.`;
|
|
29
|
+
|
|
30
|
+
export async function generateTemplateSummary(
|
|
31
|
+
messages: AgentMessage[],
|
|
32
|
+
model: Model<Api>,
|
|
33
|
+
apiKey: string,
|
|
34
|
+
promptText: string,
|
|
35
|
+
reserveTokens: number,
|
|
36
|
+
signal: AbortSignal,
|
|
37
|
+
thinkingLevel: ISummaryThinkingLevel,
|
|
38
|
+
previousSummary?: string,
|
|
39
|
+
): Promise<string> {
|
|
40
|
+
const llmMessages = convertToLlm(messages);
|
|
41
|
+
const conversationText = serializeConversation(llmMessages);
|
|
42
|
+
let fullPrompt = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
43
|
+
if (previousSummary) {
|
|
44
|
+
fullPrompt += `<previous-summary>\n${previousSummary}\n</previous-summary>\n\n`;
|
|
45
|
+
}
|
|
46
|
+
fullPrompt += promptText;
|
|
47
|
+
|
|
48
|
+
const summarizationMessages = [
|
|
49
|
+
{
|
|
50
|
+
role: "user" as const,
|
|
51
|
+
content: [{ type: "text" as const, text: fullPrompt }],
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const completionOptions = getSummarizationCompletionOptions(
|
|
57
|
+
model,
|
|
58
|
+
apiKey,
|
|
59
|
+
signal,
|
|
60
|
+
reserveTokens,
|
|
61
|
+
0.8,
|
|
62
|
+
thinkingLevel,
|
|
63
|
+
);
|
|
64
|
+
const response = await completeSimple(
|
|
65
|
+
model,
|
|
66
|
+
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
|
67
|
+
completionOptions,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (response.stopReason === "error") {
|
|
71
|
+
throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return response.content
|
|
75
|
+
.filter((content): content is { type: "text"; text: string } => content.type === "text")
|
|
76
|
+
.map((content) => content.text)
|
|
77
|
+
.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function generateTurnPrefixSummary(
|
|
81
|
+
messages: AgentMessage[],
|
|
82
|
+
model: Model<Api>,
|
|
83
|
+
apiKey: string,
|
|
84
|
+
reserveTokens: number,
|
|
85
|
+
signal: AbortSignal,
|
|
86
|
+
thinkingLevel: ISummaryThinkingLevel,
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const llmMessages = convertToLlm(messages);
|
|
89
|
+
const conversationText = serializeConversation(llmMessages);
|
|
90
|
+
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
91
|
+
|
|
92
|
+
const completionOptions = getSummarizationCompletionOptions(
|
|
93
|
+
model,
|
|
94
|
+
apiKey,
|
|
95
|
+
signal,
|
|
96
|
+
reserveTokens,
|
|
97
|
+
0.5,
|
|
98
|
+
thinkingLevel,
|
|
99
|
+
);
|
|
100
|
+
const response = await completeSimple(
|
|
101
|
+
model,
|
|
102
|
+
{
|
|
103
|
+
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
|
104
|
+
messages: [
|
|
105
|
+
{
|
|
106
|
+
role: "user" as const,
|
|
107
|
+
content: [{ type: "text" as const, text: promptText }],
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
completionOptions,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (response.stopReason === "error") {
|
|
116
|
+
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return response.content
|
|
120
|
+
.filter((content): content is { type: "text"; text: string } => content.type === "text")
|
|
121
|
+
.map((content) => content.text)
|
|
122
|
+
.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toFilePathSet(value: unknown): Set<string> {
|
|
126
|
+
if (value instanceof Set) {
|
|
127
|
+
const result = new Set<string>();
|
|
128
|
+
for (const item of value) {
|
|
129
|
+
if (typeof item === "string") {result.add(item);}
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
const result = new Set<string>();
|
|
135
|
+
for (const item of value) {
|
|
136
|
+
if (typeof item === "string") {result.add(item);}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
return new Set<string>();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function computeFileLists(fileOps: unknown): ICompactionDetails {
|
|
144
|
+
const raw = (fileOps as { read?: unknown; edited?: unknown; written?: unknown }) ?? {};
|
|
145
|
+
const read = toFilePathSet(raw.read);
|
|
146
|
+
const edited = toFilePathSet(raw.edited);
|
|
147
|
+
const written = toFilePathSet(raw.written);
|
|
148
|
+
|
|
149
|
+
const modified = new Set([...edited, ...written]);
|
|
150
|
+
const readFiles = [...read].filter((path) => !modified.has(path)).sort();
|
|
151
|
+
const modifiedFiles = [...modified].sort();
|
|
152
|
+
return { readFiles, modifiedFiles };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function formatFileOperations(details: ICompactionDetails): string {
|
|
156
|
+
const sections: string[] = [];
|
|
157
|
+
if (details.readFiles.length > 0) {
|
|
158
|
+
sections.push(`<read-files>\n${details.readFiles.join("\n")}\n</read-files>`);
|
|
159
|
+
}
|
|
160
|
+
if (details.modifiedFiles.length > 0) {
|
|
161
|
+
sections.push(`<modified-files>\n${details.modifiedFiles.join("\n")}\n</modified-files>`);
|
|
162
|
+
}
|
|
163
|
+
if (sections.length === 0) {return "";}
|
|
164
|
+
return `\n\n${sections.join("\n\n")}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getReserveTokens(event: SessionBeforeCompactEvent): number {
|
|
168
|
+
return event.preparation.settings.reserveTokens || 16_384;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getSummarizationCompletionOptions(
|
|
172
|
+
model: Model<Api>,
|
|
173
|
+
apiKey: string,
|
|
174
|
+
signal: AbortSignal,
|
|
175
|
+
reserveTokens: number,
|
|
176
|
+
ratio: number,
|
|
177
|
+
thinkingLevel: ISummaryThinkingLevel,
|
|
178
|
+
): { maxTokens: number; signal: AbortSignal; apiKey: string; reasoning?: "low" | "medium" | "high" } {
|
|
179
|
+
const maxTokens = Math.max(256, Math.floor(reserveTokens * ratio));
|
|
180
|
+
if (!model.reasoning || thinkingLevel === "off") {
|
|
181
|
+
return { maxTokens, signal, apiKey };
|
|
182
|
+
}
|
|
183
|
+
return { maxTokens, signal, apiKey, reasoning: thinkingLevel };
|
|
184
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import type {
|
|
5
|
+
ICompactionPolicy,
|
|
6
|
+
IModelEntry,
|
|
7
|
+
ISummaryPolicy,
|
|
8
|
+
ITemplateResolution,
|
|
9
|
+
} from "../policy/types.js";
|
|
10
|
+
import { resolveAgentRootPath } from "../paths.js";
|
|
11
|
+
|
|
12
|
+
const TEMPLATE_DIR = "compaction-templates";
|
|
13
|
+
const TEMPLATE_FILE = "compaction-template.md";
|
|
14
|
+
const UPDATE_TEMPLATE_FILE = "compaction-template-update.md";
|
|
15
|
+
|
|
16
|
+
type ITryReadResult = { content: string; path: string } | { error: string; path: string } | undefined;
|
|
17
|
+
|
|
18
|
+
function tryRead(path: string): ITryReadResult {
|
|
19
|
+
if (!existsSync(path)) {return undefined;}
|
|
20
|
+
try {
|
|
21
|
+
const content = readFileSync(path, "utf8").trim();
|
|
22
|
+
if (!content) {return { error: "template file is empty", path };}
|
|
23
|
+
return { content, path };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return { error: error instanceof Error ? error.message : String(error), path };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tryReadExplicit(rawPath: string): ITryReadResult {
|
|
30
|
+
const resolved = rawPath.startsWith("~/") ? join(homedir(), rawPath.slice(2)) : rawPath;
|
|
31
|
+
return tryRead(resolved) ?? { error: "file not found", path: resolved };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findTemplate(
|
|
35
|
+
cwd: string,
|
|
36
|
+
profileName: string | undefined,
|
|
37
|
+
defaultFile: string,
|
|
38
|
+
profileSuffix: string,
|
|
39
|
+
globalBase: string,
|
|
40
|
+
): ReturnType<typeof tryRead> {
|
|
41
|
+
if (profileName) {
|
|
42
|
+
const profileFile = `${profileName}${profileSuffix}.md`;
|
|
43
|
+
const projectProfile = tryRead(resolve(cwd, ".pi", TEMPLATE_DIR, profileFile));
|
|
44
|
+
if (projectProfile) {return projectProfile;}
|
|
45
|
+
const globalProfile = tryRead(join(globalBase, TEMPLATE_DIR, profileFile));
|
|
46
|
+
if (globalProfile) {return globalProfile;}
|
|
47
|
+
}
|
|
48
|
+
const projectDefault = tryRead(resolve(cwd, ".pi", defaultFile));
|
|
49
|
+
if (projectDefault) {return projectDefault;}
|
|
50
|
+
return tryRead(join(globalBase, defaultFile));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function discoverTemplate(
|
|
54
|
+
cwd: string,
|
|
55
|
+
profileName: string | undefined,
|
|
56
|
+
explicitPaths?: { template?: string; updateTemplate?: string },
|
|
57
|
+
): ITemplateResolution {
|
|
58
|
+
const globalBase = resolveAgentRootPath();
|
|
59
|
+
const initial = explicitPaths?.template
|
|
60
|
+
? tryReadExplicit(explicitPaths.template)
|
|
61
|
+
: findTemplate(cwd, profileName, TEMPLATE_FILE, "", globalBase);
|
|
62
|
+
if (!initial) {return {};}
|
|
63
|
+
if ("error" in initial) {return { resolvedPath: initial.path, fallbackReason: initial.error };}
|
|
64
|
+
|
|
65
|
+
const result: ITemplateResolution = { template: initial.content, resolvedPath: initial.path };
|
|
66
|
+
|
|
67
|
+
const update = explicitPaths?.updateTemplate
|
|
68
|
+
? tryReadExplicit(explicitPaths.updateTemplate)
|
|
69
|
+
: findTemplate(cwd, profileName, UPDATE_TEMPLATE_FILE, "-update", globalBase);
|
|
70
|
+
if (update) {
|
|
71
|
+
if ("content" in update) {
|
|
72
|
+
result.updateTemplate = update.content;
|
|
73
|
+
result.updateResolvedPath = update.path;
|
|
74
|
+
} else {
|
|
75
|
+
result.updateResolvedPath = update.path;
|
|
76
|
+
result.updateFallbackReason = update.error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveSummarySettings(policy: ICompactionPolicy, entry: IModelEntry): ISummaryPolicy {
|
|
84
|
+
return {
|
|
85
|
+
thinkingLevel: entry.thinkingLevel ?? policy.summary.thinkingLevel,
|
|
86
|
+
preservationInstruction: entry.preservationInstruction ?? policy.summary.preservationInstruction,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildSummaryPrompt(
|
|
91
|
+
template: string,
|
|
92
|
+
updateTemplate: string | undefined,
|
|
93
|
+
previousSummary: string | undefined,
|
|
94
|
+
customInstructions: string | undefined,
|
|
95
|
+
preservationInstruction: string,
|
|
96
|
+
): string {
|
|
97
|
+
const promptParts: string[] = [];
|
|
98
|
+
if (previousSummary) {
|
|
99
|
+
promptParts.push(
|
|
100
|
+
"The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.",
|
|
101
|
+
"",
|
|
102
|
+
"Update the existing structured summary with new information. RULES:",
|
|
103
|
+
"- PRESERVE all existing information from the previous summary",
|
|
104
|
+
"- ADD new progress, decisions, and context from the new messages",
|
|
105
|
+
"- Move completed items to Done, update Next Steps based on progress",
|
|
106
|
+
"- If something is no longer relevant, you may remove it",
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
promptParts.push(
|
|
110
|
+
"The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const activeTemplate = previousSummary && updateTemplate ? updateTemplate : template;
|
|
115
|
+
promptParts.push("", "Use this EXACT format:", "", activeTemplate, "", "Keep each section concise.");
|
|
116
|
+
if (preservationInstruction) {
|
|
117
|
+
promptParts.push(preservationInstruction);
|
|
118
|
+
}
|
|
119
|
+
if (customInstructions) {
|
|
120
|
+
promptParts.push("", `Additional focus: ${customInstructions}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return promptParts.join("\n");
|
|
124
|
+
}
|