pi-sessions 0.4.0 → 0.4.1
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.
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type Tool,
|
|
10
|
-
type ToolCall,
|
|
2
|
+
import type {
|
|
3
|
+
Api,
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
Model,
|
|
6
|
+
TextContent,
|
|
7
|
+
ThinkingContent,
|
|
8
|
+
ToolCall,
|
|
11
9
|
} from "@earendil-works/pi-ai";
|
|
12
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
13
11
|
import {
|
|
14
12
|
buildSessionContext,
|
|
15
13
|
convertToLlm,
|
|
14
|
+
createAgentSession,
|
|
15
|
+
DefaultResourceLoader,
|
|
16
|
+
defineTool,
|
|
17
|
+
getAgentDir,
|
|
18
|
+
SessionManager,
|
|
16
19
|
serializeConversation,
|
|
17
20
|
} from "@earendil-works/pi-coding-agent";
|
|
18
21
|
import { type Static, Type } from "typebox";
|
|
@@ -56,12 +59,7 @@ const HANDOFF_EXTRACTION_PARAMETERS = Type.Object({
|
|
|
56
59
|
),
|
|
57
60
|
});
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
name: "create_handoff_context",
|
|
61
|
-
description: "Extract the structured handoff context for the next session.",
|
|
62
|
-
parameters: HANDOFF_EXTRACTION_PARAMETERS,
|
|
63
|
-
};
|
|
64
|
-
|
|
62
|
+
type HandoffExtractionArgs = Static<typeof HANDOFF_EXTRACTION_PARAMETERS>;
|
|
65
63
|
type RequiredHandoffExtractionArgs = Static<typeof REQUIRED_HANDOFF_EXTRACTION_PARAMETERS>;
|
|
66
64
|
|
|
67
65
|
const REQUIRED_HANDOFF_EXTRACTION_PARAMETERS = Type.Object({
|
|
@@ -95,11 +93,7 @@ export async function generateHandoffDraft(
|
|
|
95
93
|
throw new Error("No model is available for handoff.");
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
const
|
|
99
|
-
if (!auth.ok || !auth.apiKey) {
|
|
100
|
-
throw new Error(`No API key is available for ${ctx.model.provider}/${ctx.model.id}.`);
|
|
101
|
-
}
|
|
102
|
-
|
|
96
|
+
const model = ctx.model;
|
|
103
97
|
const sessionContext = buildSessionContext(
|
|
104
98
|
ctx.sessionManager.getEntries(),
|
|
105
99
|
ctx.sessionManager.getLeafId(),
|
|
@@ -109,51 +103,18 @@ export async function generateHandoffDraft(
|
|
|
109
103
|
}
|
|
110
104
|
|
|
111
105
|
const conversationText = serializeConversation(convertToLlm(sessionContext.messages));
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
],
|
|
120
|
-
timestamp: Date.now(),
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const requestOptions: SimpleStreamOptions = {
|
|
124
|
-
apiKey: auth.apiKey,
|
|
125
|
-
...(auth.headers ? { headers: auth.headers } : {}),
|
|
126
|
-
...(signal ? { signal } : {}),
|
|
127
|
-
...(ctx.model.reasoning && thinkingLevel && thinkingLevel !== "off"
|
|
128
|
-
? { reasoning: thinkingLevel }
|
|
129
|
-
: {}),
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const response = await completeSimple(
|
|
133
|
-
ctx.model,
|
|
134
|
-
{
|
|
135
|
-
systemPrompt: HANDOFF_SYSTEM_PROMPT,
|
|
136
|
-
messages: [userMessage],
|
|
137
|
-
tools: [HANDOFF_EXTRACTION_TOOL],
|
|
138
|
-
},
|
|
139
|
-
requestOptions,
|
|
106
|
+
const handoffContext = await runHandoffExtractionAgent(
|
|
107
|
+
ctx,
|
|
108
|
+
model,
|
|
109
|
+
conversationText,
|
|
110
|
+
goal,
|
|
111
|
+
thinkingLevel,
|
|
112
|
+
signal,
|
|
140
113
|
);
|
|
141
|
-
|
|
142
|
-
if (response.stopReason === "aborted") {
|
|
114
|
+
if (!handoffContext) {
|
|
143
115
|
return undefined;
|
|
144
116
|
}
|
|
145
117
|
|
|
146
|
-
if (response.stopReason === "error") {
|
|
147
|
-
throw new Error(response.errorMessage ?? "Handoff generation failed.");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const extraction = extractHandoffContext(response, goal);
|
|
151
|
-
if (!extraction.context) {
|
|
152
|
-
throw new Error(extraction.error);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const handoffContext = extraction.context;
|
|
156
|
-
|
|
157
118
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
158
119
|
const sessionPath = ctx.sessionManager.getSessionFile();
|
|
159
120
|
|
|
@@ -177,6 +138,84 @@ export function buildExtractionPrompt(conversationText: string, goal: string): s
|
|
|
177
138
|
].join("\n");
|
|
178
139
|
}
|
|
179
140
|
|
|
141
|
+
async function runHandoffExtractionAgent(
|
|
142
|
+
ctx: ExtensionContext,
|
|
143
|
+
model: Model<Api>,
|
|
144
|
+
conversationText: string,
|
|
145
|
+
goal: string,
|
|
146
|
+
thinkingLevel: ThinkingLevel | undefined,
|
|
147
|
+
signal?: AbortSignal,
|
|
148
|
+
): Promise<HandoffContext | undefined> {
|
|
149
|
+
let capturedArguments: HandoffExtractionArgs | undefined;
|
|
150
|
+
const createHandoffContextTool = defineTool({
|
|
151
|
+
name: "create_handoff_context",
|
|
152
|
+
label: "Create handoff context",
|
|
153
|
+
description: "Extract the structured handoff context for the next session.",
|
|
154
|
+
parameters: HANDOFF_EXTRACTION_PARAMETERS,
|
|
155
|
+
execute: async (_toolCallId, params) => {
|
|
156
|
+
capturedArguments = params;
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: "Handoff context captured. Stopping." }],
|
|
159
|
+
details: {},
|
|
160
|
+
terminate: true,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
166
|
+
cwd: ctx.cwd,
|
|
167
|
+
agentDir: getAgentDir(),
|
|
168
|
+
noExtensions: true,
|
|
169
|
+
noPromptTemplates: true,
|
|
170
|
+
noSkills: true,
|
|
171
|
+
appendSystemPromptOverride: (base) => [...base, HANDOFF_SYSTEM_PROMPT],
|
|
172
|
+
});
|
|
173
|
+
await resourceLoader.reload();
|
|
174
|
+
|
|
175
|
+
const { session } = await createAgentSession({
|
|
176
|
+
cwd: ctx.cwd,
|
|
177
|
+
model,
|
|
178
|
+
modelRegistry: ctx.modelRegistry,
|
|
179
|
+
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
180
|
+
tools: ["read", "grep", "find", "ls", "create_handoff_context"],
|
|
181
|
+
customTools: [createHandoffContextTool],
|
|
182
|
+
resourceLoader,
|
|
183
|
+
sessionManager: SessionManager.inMemory(ctx.cwd),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const abortHandler = (): void => {
|
|
187
|
+
void session.abort();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
signal?.addEventListener("abort", abortHandler, { once: true });
|
|
192
|
+
if (signal?.aborted) {
|
|
193
|
+
await session.abort();
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await session.prompt(buildExtractionPrompt(conversationText, goal));
|
|
198
|
+
} finally {
|
|
199
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
200
|
+
session.dispose();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (signal?.aborted) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!capturedArguments) {
|
|
208
|
+
throw new Error("Handoff extraction did not return structured context.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const extraction = extractHandoffContextFromArguments(capturedArguments, goal);
|
|
212
|
+
if (!extraction.context) {
|
|
213
|
+
throw new Error(extraction.error);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return extraction.context;
|
|
217
|
+
}
|
|
218
|
+
|
|
180
219
|
export function assembleHandoffDraft(
|
|
181
220
|
sessionId: string,
|
|
182
221
|
sessionPath: string | undefined,
|
|
@@ -224,11 +263,18 @@ export function extractHandoffContext(
|
|
|
224
263
|
return { error: "Handoff extraction did not return structured context." };
|
|
225
264
|
}
|
|
226
265
|
|
|
266
|
+
return extractHandoffContextFromArguments(toolCall.arguments, goal);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function extractHandoffContextFromArguments(
|
|
270
|
+
argumentsValue: unknown,
|
|
271
|
+
goal: string,
|
|
272
|
+
): { context: HandoffContext; error?: undefined } | { context?: undefined; error: string } {
|
|
227
273
|
let requiredArguments: RequiredHandoffExtractionArgs;
|
|
228
274
|
try {
|
|
229
275
|
requiredArguments = parseTypeBoxValue(
|
|
230
276
|
REQUIRED_HANDOFF_EXTRACTION_PARAMETERS,
|
|
231
|
-
|
|
277
|
+
argumentsValue,
|
|
232
278
|
"Invalid create_handoff_context arguments",
|
|
233
279
|
);
|
|
234
280
|
} catch {
|
|
@@ -241,9 +287,9 @@ export function extractHandoffContext(
|
|
|
241
287
|
}
|
|
242
288
|
|
|
243
289
|
const summary = normalizeText(requiredArguments.summary);
|
|
244
|
-
const relevantFiles =
|
|
290
|
+
const relevantFiles = getRelevantFiles(argumentsValue);
|
|
245
291
|
const nextTask = normalizeText(requiredArguments.nextTask) || goal.trim();
|
|
246
|
-
const openQuestions =
|
|
292
|
+
const openQuestions = getOpenQuestions(argumentsValue);
|
|
247
293
|
|
|
248
294
|
if (!summary || !nextTask || !title) {
|
|
249
295
|
return { error: "Handoff extraction did not return structured context." };
|
|
@@ -285,10 +331,30 @@ function normalizeStringArray(value: unknown, limit: number): string[] {
|
|
|
285
331
|
return [...uniqueValues];
|
|
286
332
|
}
|
|
287
333
|
|
|
334
|
+
function getRelevantFiles(argumentsValue: unknown): string[] {
|
|
335
|
+
if (!isRecord(argumentsValue)) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return normalizeStringArray(argumentsValue.relevantFiles, MAX_RELEVANT_FILES);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getOpenQuestions(argumentsValue: unknown): string[] {
|
|
343
|
+
if (!isRecord(argumentsValue)) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return normalizeStringArray(argumentsValue.openQuestions, MAX_OPEN_QUESTIONS);
|
|
348
|
+
}
|
|
349
|
+
|
|
288
350
|
function normalizeText(value: unknown): string {
|
|
289
351
|
return typeof value === "string" ? value.trim() : "";
|
|
290
352
|
}
|
|
291
353
|
|
|
354
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
355
|
+
return typeof value === "object" && value !== null;
|
|
356
|
+
}
|
|
357
|
+
|
|
292
358
|
function isCreateHandoffContextToolCall(
|
|
293
359
|
content: TextContent | ThinkingContent | ToolCall,
|
|
294
360
|
): content is ToolCall {
|