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
- type AssistantMessage,
4
- completeSimple,
5
- type Message,
6
- type SimpleStreamOptions,
7
- type TextContent,
8
- type ThinkingContent,
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
- const HANDOFF_EXTRACTION_TOOL: Tool<typeof HANDOFF_EXTRACTION_PARAMETERS> = {
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 auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
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 userMessage: Message = {
113
- role: "user",
114
- content: [
115
- {
116
- type: "text",
117
- text: buildExtractionPrompt(conversationText, goal),
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
- toolCall.arguments,
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 = normalizeStringArray(toolCall.arguments.relevantFiles, MAX_RELEVANT_FILES);
290
+ const relevantFiles = getRelevantFiles(argumentsValue);
245
291
  const nextTask = normalizeText(requiredArguments.nextTask) || goal.trim();
246
- const openQuestions = normalizeStringArray(toolCall.arguments.openQuestions, MAX_OPEN_QUESTIONS);
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, auto-titling, and indexing tools",
6
6
  "license": "MIT",