pi-sessions 0.6.0 → 0.7.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.
- package/README.md +22 -11
- package/extensions/session-ask/agent.ts +400 -0
- package/extensions/session-ask/navigate.ts +880 -0
- package/extensions/session-ask.ts +79 -131
- package/extensions/session-auto-title/model.ts +3 -11
- package/extensions/session-handoff/picker.ts +50 -4
- package/extensions/session-handoff/query.ts +74 -56
- package/extensions/session-handoff/refs.ts +12 -18
- package/extensions/session-handoff.ts +36 -27
- package/extensions/session-messaging/broker/process.ts +23 -26
- package/extensions/session-messaging/broker/spawn.ts +24 -3
- package/extensions/session-messaging/pi/incoming-runtime.ts +2 -18
- package/extensions/session-messaging/pi/message-contracts.ts +9 -10
- package/extensions/session-messaging/pi/message-view.ts +12 -1
- package/extensions/session-messaging/pi/service.ts +117 -62
- package/extensions/session-messaging/pi/tools.ts +4 -56
- package/extensions/session-search/extract.ts +86 -436
- package/extensions/session-search/hooks.ts +17 -25
- package/extensions/session-search/normalize.ts +1 -1
- package/extensions/session-search/reindex.ts +1 -0
- package/extensions/session-search.ts +157 -128
- package/extensions/shared/model.ts +18 -0
- package/extensions/shared/session-broker/active.ts +26 -0
- package/extensions/{session-messaging/pi → shared/session-broker}/client.ts +16 -19
- package/extensions/{session-messaging/shared → shared/session-broker}/framing.ts +1 -1
- package/extensions/{session-messaging/shared → shared/session-broker}/protocol.ts +5 -12
- package/extensions/shared/session-index/access.ts +128 -0
- package/extensions/shared/session-index/common.ts +43 -48
- package/extensions/shared/session-index/index.ts +1 -0
- package/extensions/shared/session-index/lineage.ts +10 -0
- package/extensions/shared/session-index/query/ast.ts +40 -0
- package/extensions/shared/session-index/query/compiler.ts +146 -0
- package/extensions/shared/session-index/query/lexer.ts +140 -0
- package/extensions/shared/session-index/query/parser.ts +178 -0
- package/extensions/shared/session-index/schema.ts +22 -6
- package/extensions/shared/session-index/scoring.ts +80 -0
- package/extensions/shared/session-index/search.ts +552 -278
- package/extensions/shared/session-index/sqlite.ts +16 -9
- package/extensions/shared/session-index/store.ts +12 -21
- package/extensions/shared/settings.ts +61 -4
- package/extensions/shared/text.ts +50 -0
- package/package.json +5 -4
- /package/extensions/{session-messaging/shared → shared/session-broker}/socket-path.ts +0 -0
package/README.md
CHANGED
|
@@ -51,15 +51,26 @@ What session did I implement the db layer?
|
|
|
51
51
|
|
|
52
52
|
## Features
|
|
53
53
|
|
|
54
|
-
| Extension | Surface
|
|
55
|
-
| ------------------ |
|
|
56
|
-
| Session Search | `session_search` pi tool
|
|
57
|
-
| Session Ask | `session_ask` pi tool
|
|
58
|
-
| Session Handoff | `/handoff`, `session_handoff` pi tool
|
|
59
|
-
| Session Messaging | `
|
|
60
|
-
| Session Picker | `Alt+O`
|
|
61
|
-
| Session Index | `/session-index` slash command
|
|
62
|
-
| Session Auto Title | in background, `/title` slash command
|
|
54
|
+
| Extension | Surface | What it does |
|
|
55
|
+
| ------------------ | ------------------------------------- | ------------------------------------------------------- |
|
|
56
|
+
| Session Search | `session_search` pi tool | Search through old sessions |
|
|
57
|
+
| Session Ask | `session_ask` pi tool | Ask questions about old sessions |
|
|
58
|
+
| Session Handoff | `/handoff`, `session_handoff` pi tool | Start a focused new session; alternative to compaction |
|
|
59
|
+
| Session Messaging | `session_send_message` pi tool | Send messages between running Pi sessions |
|
|
60
|
+
| Session Picker | `Alt+O` | Reference old sessions in your prompt |
|
|
61
|
+
| Session Index | `/session-index` slash command | Shows index status and rebuilds the local session index |
|
|
62
|
+
| Session Auto Title | in background, `/title` slash command | Give sessions titles |
|
|
63
|
+
|
|
64
|
+
## Session Search
|
|
65
|
+
|
|
66
|
+
`session_search` searches the local session index by text, repo, cwd, time range, file evidence, and whether a session is currently running.
|
|
67
|
+
|
|
68
|
+
Queries support regular text for normal usage, quoted phrases, `AND` / `OR` / `NOT`, parentheses, and `-term` negation when matching needs to be stricter. Unquoted terms use prefix matching, quoted terms are exact. A search with no query returns matching sessions chronologically, newest first. Use `live: true` to restrict results to currently running sessions.
|
|
69
|
+
|
|
70
|
+
File filters distinguish read-or-write evidence from write-only evidence:
|
|
71
|
+
|
|
72
|
+
- `files.touched`: sessions that read or changed a path
|
|
73
|
+
- `files.changed`: sessions that changed a path
|
|
63
74
|
|
|
64
75
|
## Session Handoff
|
|
65
76
|
|
|
@@ -91,8 +102,8 @@ If background handoffs ever target the wrong pane, run `/handoff --identify` fro
|
|
|
91
102
|
|
|
92
103
|
Agents can coordinate with other currently running Pi sessions:
|
|
93
104
|
|
|
94
|
-
- `
|
|
95
|
-
- `session_send_message` sends a message to
|
|
105
|
+
- `session_search` with `live: true` lists live sessions
|
|
106
|
+
- `session_send_message` sends a message to another live session
|
|
96
107
|
|
|
97
108
|
Incoming messages start the recipient agent when idle and steer it when already running. Inactive sessions cannot receive messages, but you can still use `session_search` and `session_ask` with them.
|
|
98
109
|
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
createAgentSession,
|
|
5
|
+
DefaultResourceLoader,
|
|
6
|
+
defineTool,
|
|
7
|
+
getAgentDir,
|
|
8
|
+
SessionManager,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { type Static, Type } from "typebox";
|
|
11
|
+
import { resolveConfiguredModel } from "../shared/model.ts";
|
|
12
|
+
import {
|
|
13
|
+
type SessionLineageRow,
|
|
14
|
+
searchSessionChunks,
|
|
15
|
+
withSessionIndex,
|
|
16
|
+
} from "../shared/session-index/index.ts";
|
|
17
|
+
import { type AskSettings, getDefaultSessionAskRunsDir } from "../shared/settings.ts";
|
|
18
|
+
import {
|
|
19
|
+
buildSessionMap,
|
|
20
|
+
buildSessionMetadata,
|
|
21
|
+
findBranchesForEntry,
|
|
22
|
+
findSpanForEntry,
|
|
23
|
+
loadSessionNavigationData,
|
|
24
|
+
readEntriesFromEntry,
|
|
25
|
+
readProjectAgentsMd,
|
|
26
|
+
type SessionNavigationData,
|
|
27
|
+
type SessionReadResult,
|
|
28
|
+
type SessionSearchHitBranch,
|
|
29
|
+
type SessionSearchHitSpan,
|
|
30
|
+
} from "./navigate.ts";
|
|
31
|
+
|
|
32
|
+
const SEARCH_SESSION_TOOL_NAME = "session_search";
|
|
33
|
+
const SESSION_READ_TOOL_NAME = "session_read";
|
|
34
|
+
const PROVIDE_RESULTS_TOOL_NAME = "provide_results";
|
|
35
|
+
const SESSION_ASK_AGENT_TOOLS = [
|
|
36
|
+
"read",
|
|
37
|
+
"grep",
|
|
38
|
+
"find",
|
|
39
|
+
"ls",
|
|
40
|
+
SEARCH_SESSION_TOOL_NAME,
|
|
41
|
+
SESSION_READ_TOOL_NAME,
|
|
42
|
+
PROVIDE_RESULTS_TOOL_NAME,
|
|
43
|
+
];
|
|
44
|
+
const MAX_SESSION_ASK_ATTEMPTS = 3;
|
|
45
|
+
|
|
46
|
+
const SESSION_ASK_NAVIGATION_SYSTEM_PROMPT = `You are an evidence-first session analyst. Your job is to inspect one Pi session, gather the relevant context from its spans and entries, and answer the question below only after verifying the original evidence.
|
|
47
|
+
|
|
48
|
+
Rules:
|
|
49
|
+
- Do not stop at the first relevant hit. Check newer messages that revise, supersede, revert, or contradict it.
|
|
50
|
+
- Tool calls record attempted actions, not outcomes. Check tool results before treating an action as successful.
|
|
51
|
+
- Keep session history and current repo state separate. The repo may have changed. Say "the session says" and "the current repo confirms/contradicts" when both matter.
|
|
52
|
+
- Compaction summaries are navigation aids, not answer sources. Use original entries for exact requirements, decisions, commands, and outcomes whenever they exist.
|
|
53
|
+
- Call provide_results exactly once with the answer.`;
|
|
54
|
+
|
|
55
|
+
const SEARCH_SESSION_PARAMETERS = Type.Object({
|
|
56
|
+
query: Type.String({
|
|
57
|
+
description:
|
|
58
|
+
"Use plain adjacent terms for normal search within the target session. Supports quoted phrases, AND/OR/NOT, parentheses, and -term negation when matching needs to be stricter.",
|
|
59
|
+
}),
|
|
60
|
+
limit: Type.Optional(Type.Number({ description: "Maximum hits to return." })),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const SESSION_READ_PARAMETERS = Type.Object({
|
|
64
|
+
entryId: Type.String({ description: "Entry id to start from." }),
|
|
65
|
+
pathTarget: Type.Optional(
|
|
66
|
+
Type.String({
|
|
67
|
+
description: "Returns entries from `entryId` through this entryId along that path.",
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
before: Type.Optional(Type.Number({ description: "Entries before entryId to include." })),
|
|
71
|
+
after: Type.Optional(Type.Number({ description: "Entries after entryId to include." })),
|
|
72
|
+
body: Type.Optional(
|
|
73
|
+
Type.Union([Type.Literal("preview"), Type.Literal("full")], {
|
|
74
|
+
description:
|
|
75
|
+
'"preview" (default) truncates tool calls, enabling safer larger-range queries. When a particular entry is of interest, read a targeted, small range with "full" to get the entire entry.',
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const PROVIDE_RESULTS_PARAMETERS = Type.Object({
|
|
81
|
+
answer: Type.String({
|
|
82
|
+
description: "The final answer to return to the caller, in markdown.",
|
|
83
|
+
}),
|
|
84
|
+
relevantFiles: Type.Optional(
|
|
85
|
+
Type.Array(
|
|
86
|
+
Type.Object({
|
|
87
|
+
path: Type.String({
|
|
88
|
+
description: "Absolute path to a file that matters to the answer.",
|
|
89
|
+
}),
|
|
90
|
+
reason: Type.String({
|
|
91
|
+
description: "Why this file matters to the answer.",
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
type SearchSessionParams = Static<typeof SEARCH_SESSION_PARAMETERS>;
|
|
99
|
+
type SessionReadParams = Static<typeof SESSION_READ_PARAMETERS>;
|
|
100
|
+
type ProvideResultsArgs = Static<typeof PROVIDE_RESULTS_PARAMETERS>;
|
|
101
|
+
|
|
102
|
+
interface SearchSessionToolDetails {
|
|
103
|
+
hits: SessionAskSearchHit[];
|
|
104
|
+
searchUnavailable: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface SessionAskSearchHit {
|
|
108
|
+
entryId: string;
|
|
109
|
+
sourceKind: string;
|
|
110
|
+
timestamp: string;
|
|
111
|
+
size: number;
|
|
112
|
+
snippet: string;
|
|
113
|
+
branch: SessionSearchHitBranch[];
|
|
114
|
+
span?: SessionSearchHitSpan | undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SessionAskAgentResult {
|
|
118
|
+
answer: string;
|
|
119
|
+
relevantFiles: Array<{ path: string; reason: string }>;
|
|
120
|
+
debugSessionPath?: string | undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function runSessionAskAgent(params: {
|
|
124
|
+
ctx: ExtensionContext;
|
|
125
|
+
target: SessionLineageRow;
|
|
126
|
+
question: string;
|
|
127
|
+
indexPath: string;
|
|
128
|
+
askSettings: AskSettings | undefined;
|
|
129
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
130
|
+
signal?: AbortSignal | undefined;
|
|
131
|
+
}): Promise<SessionAskAgentResult | undefined> {
|
|
132
|
+
const navigationData = loadSessionNavigationData(params.target.sessionPath);
|
|
133
|
+
const model =
|
|
134
|
+
resolveConfiguredModel(params.ctx.modelRegistry.getAvailable(), params.askSettings?.model) ??
|
|
135
|
+
params.ctx.model;
|
|
136
|
+
if (!model) {
|
|
137
|
+
throw new Error("No active model is available for session_ask.");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let capturedArguments: ProvideResultsArgs | undefined;
|
|
141
|
+
const searchSessionTool = defineTool({
|
|
142
|
+
name: SEARCH_SESSION_TOOL_NAME,
|
|
143
|
+
label: "Search target session",
|
|
144
|
+
description: "Search the target session and return ranked hits.",
|
|
145
|
+
promptSnippet: "Search target session",
|
|
146
|
+
promptGuidelines: [
|
|
147
|
+
"Results are sorted best-first by relevance.",
|
|
148
|
+
"Use returned entryId values with session_read to inspect full session context.",
|
|
149
|
+
],
|
|
150
|
+
parameters: SEARCH_SESSION_PARAMETERS,
|
|
151
|
+
execute: async (_toolCallId, toolParams: SearchSessionParams) => {
|
|
152
|
+
const hits = withSessionIndex(params.indexPath, { mode: "read", required: false }, ({ db }) =>
|
|
153
|
+
searchSessionChunks(db, {
|
|
154
|
+
sessionIds: [params.target.sessionId],
|
|
155
|
+
query: toolParams.query,
|
|
156
|
+
limit: toolParams.limit ?? 50,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (!hits) {
|
|
161
|
+
const noHits: SessionAskSearchHit[] = [];
|
|
162
|
+
const searchUnavailable: boolean = true;
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text" as const,
|
|
167
|
+
text: "Session search is unavailable because the session index could not be opened. Navigate with the Session Map and session_read instead.",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
details: { hits: noHits, searchUnavailable } as SearchSessionToolDetails,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const formattedHits = hits.map((hit) => ({
|
|
175
|
+
entryId: hit.entryId,
|
|
176
|
+
sourceKind: hit.sourceKind,
|
|
177
|
+
timestamp: hit.ts,
|
|
178
|
+
size: navigationData.entrySizes.get(hit.entryId) ?? 0,
|
|
179
|
+
snippet: hit.snippet,
|
|
180
|
+
branch: findBranchesForEntry(navigationData, hit.entryId),
|
|
181
|
+
span: findSpanForEntry(navigationData, hit.entryId),
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text" as const,
|
|
188
|
+
text: formatSearchSessionHits(formattedHits),
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
details: {
|
|
192
|
+
hits: formattedHits,
|
|
193
|
+
searchUnavailable: false as boolean,
|
|
194
|
+
} as SearchSessionToolDetails,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const sessionReadTool = defineTool({
|
|
200
|
+
name: SESSION_READ_TOOL_NAME,
|
|
201
|
+
label: "Read target session entries",
|
|
202
|
+
description: "Read full entry content from the target session, starting at an entry id.",
|
|
203
|
+
promptSnippet: "Read target session entries",
|
|
204
|
+
promptGuidelines: [
|
|
205
|
+
"Use either pathTarget or before/after; they are mutually exclusive.",
|
|
206
|
+
"Read an entire span by using the span's end id as the pathTarget.",
|
|
207
|
+
"Read across spans by using a later connected span's end id as the pathTarget.",
|
|
208
|
+
"Combine before and/or after for local context around a specific entryId; must stay within the containing span.",
|
|
209
|
+
],
|
|
210
|
+
parameters: SESSION_READ_PARAMETERS,
|
|
211
|
+
execute: async (_toolCallId, toolParams: SessionReadParams) => {
|
|
212
|
+
const readResult = readEntriesFromEntry(navigationData, {
|
|
213
|
+
entryId: toolParams.entryId,
|
|
214
|
+
before: toolParams.before,
|
|
215
|
+
after: toolParams.after,
|
|
216
|
+
pathTargetEntryId: toolParams.pathTarget,
|
|
217
|
+
body: toolParams.body,
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: "text" as const,
|
|
223
|
+
text: formatSessionReadResult(readResult),
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
details: readResult,
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const provideResultsTool = defineTool({
|
|
232
|
+
name: PROVIDE_RESULTS_TOOL_NAME,
|
|
233
|
+
label: "Provide session ask result",
|
|
234
|
+
description: "Return the final answer for session_ask.",
|
|
235
|
+
promptSnippet: "Provide session ask result",
|
|
236
|
+
promptGuidelines: [
|
|
237
|
+
"Call provide_results exactly once when you are ready to answer. It is the final tool call and ends the session_ask sub-agent turn.",
|
|
238
|
+
],
|
|
239
|
+
parameters: PROVIDE_RESULTS_PARAMETERS,
|
|
240
|
+
execute: async (_toolCallId, toolParams: ProvideResultsArgs) => {
|
|
241
|
+
capturedArguments = toolParams;
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text" as const,
|
|
246
|
+
text: "Session ask result captured. Stopping.",
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
details: toolParams,
|
|
250
|
+
terminate: true,
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const cwd = navigationData.header.cwd;
|
|
256
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
257
|
+
cwd,
|
|
258
|
+
agentDir: getAgentDir(),
|
|
259
|
+
noExtensions: true,
|
|
260
|
+
noPromptTemplates: true,
|
|
261
|
+
noSkills: true,
|
|
262
|
+
appendSystemPromptOverride: (base) => [...base, SESSION_ASK_NAVIGATION_SYSTEM_PROMPT],
|
|
263
|
+
});
|
|
264
|
+
await resourceLoader.reload();
|
|
265
|
+
|
|
266
|
+
const thinkingLevel = params.askSettings?.thinkingLevel ?? params.thinkingLevel;
|
|
267
|
+
const sessionManager = params.askSettings?.persistRuns
|
|
268
|
+
? SessionManager.create(cwd, getDefaultSessionAskRunsDir())
|
|
269
|
+
: SessionManager.inMemory(cwd);
|
|
270
|
+
const debugSessionPath = params.askSettings?.persistRuns
|
|
271
|
+
? sessionManager.getSessionFile()
|
|
272
|
+
: undefined;
|
|
273
|
+
const { session } = await createAgentSession({
|
|
274
|
+
cwd,
|
|
275
|
+
model,
|
|
276
|
+
modelRegistry: params.ctx.modelRegistry,
|
|
277
|
+
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
278
|
+
tools: SESSION_ASK_AGENT_TOOLS,
|
|
279
|
+
customTools: [searchSessionTool, sessionReadTool, provideResultsTool],
|
|
280
|
+
resourceLoader,
|
|
281
|
+
sessionManager,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const abortHandler = (): void => {
|
|
285
|
+
void session.abort();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
params.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
290
|
+
if (params.signal?.aborted) {
|
|
291
|
+
await session.abort();
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (let attempt = 1; attempt <= MAX_SESSION_ASK_ATTEMPTS; attempt += 1) {
|
|
296
|
+
const prompt =
|
|
297
|
+
attempt === 1
|
|
298
|
+
? buildNavigationPrompt(navigationData, params.question)
|
|
299
|
+
: `You did not call ${PROVIDE_RESULTS_TOOL_NAME}. Continue from your prior work and call ${PROVIDE_RESULTS_TOOL_NAME} exactly once with the final markdown answer.`;
|
|
300
|
+
await session.prompt(prompt);
|
|
301
|
+
if (capturedArguments) {
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} finally {
|
|
306
|
+
params.signal?.removeEventListener("abort", abortHandler);
|
|
307
|
+
session.dispose();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!capturedArguments) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
answer: capturedArguments.answer.trim(),
|
|
316
|
+
relevantFiles: capturedArguments.relevantFiles ?? [],
|
|
317
|
+
...(debugSessionPath ? { debugSessionPath } : {}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function buildNavigationPrompt(data: SessionNavigationData, question: string): string {
|
|
322
|
+
const metadata = buildSessionMetadata(data);
|
|
323
|
+
const agentsMd = readProjectAgentsMd(data.header.cwd) ?? "<file not found>";
|
|
324
|
+
|
|
325
|
+
return `## Target Session
|
|
326
|
+
session_id: ${metadata.sessionId}
|
|
327
|
+
title: ${metadata.sessionName || "[unnamed]"}
|
|
328
|
+
cwd: ${metadata.cwd}
|
|
329
|
+
started_at: ${metadata.startedAt}
|
|
330
|
+
modified_at: ${metadata.modifiedAt}
|
|
331
|
+
entry_count: ${metadata.entryCount}
|
|
332
|
+
message_count: ${metadata.messageCount}
|
|
333
|
+
span_count: ${data.spans.length}
|
|
334
|
+
|
|
335
|
+
## Session Map
|
|
336
|
+
${buildSessionMap(data)}
|
|
337
|
+
|
|
338
|
+
## Project AGENTS.md
|
|
339
|
+
\`\`\`\`md
|
|
340
|
+
${agentsMd}
|
|
341
|
+
\`\`\`\`
|
|
342
|
+
|
|
343
|
+
## Question
|
|
344
|
+
${question}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function formatSearchSessionHits(hits: SessionAskSearchHit[]): string {
|
|
348
|
+
if (hits.length === 0) {
|
|
349
|
+
return "No matching entries found.";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return hits
|
|
353
|
+
.map((hit) =>
|
|
354
|
+
[
|
|
355
|
+
`### ${hit.entryId}`,
|
|
356
|
+
`source_kind: ${hit.sourceKind}`,
|
|
357
|
+
`timestamp: ${hit.timestamp}`,
|
|
358
|
+
`size: ${hit.size}`,
|
|
359
|
+
`branches: ${hit.branch.map((branch) => `${branch.branchLeafId}${branch.isActive ? "*" : ""}`).join(", ") || "none"}`,
|
|
360
|
+
`span: ${formatSearchHitSpan(hit.span)}`,
|
|
361
|
+
"snippet:",
|
|
362
|
+
"````",
|
|
363
|
+
hit.snippet,
|
|
364
|
+
"````",
|
|
365
|
+
].join("\n"),
|
|
366
|
+
)
|
|
367
|
+
.join("\n\n");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function formatSearchHitSpan(span: SessionSearchHitSpan | undefined): string {
|
|
371
|
+
if (!span) {
|
|
372
|
+
return "none";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return `(${span.startsAtEntryId}, ${span.endsAtEntryId})${span.isActive ? " on_active_branch" : ""}; before ${span.entriesBefore}; after ${span.entriesAfter}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function formatSessionReadResult(result: SessionReadResult): string {
|
|
379
|
+
const sections = result.entries.map((entry) =>
|
|
380
|
+
[
|
|
381
|
+
`### ${entry.entryId}`,
|
|
382
|
+
`type: ${entry.type}`,
|
|
383
|
+
`timestamp: ${entry.timestamp}`,
|
|
384
|
+
`path_target: ${entry.pathTargetEntryId}`,
|
|
385
|
+
`body: ${entry.body}`,
|
|
386
|
+
`size: ${entry.size}`,
|
|
387
|
+
entry.truncated ? "truncated: true" : "truncated: false",
|
|
388
|
+
"",
|
|
389
|
+
entry.content,
|
|
390
|
+
].join("\n"),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
if (result.nextEntryId) {
|
|
394
|
+
sections.push(
|
|
395
|
+
`Next page: call session_read with entryId ${result.nextEntryId} and pathTarget ${result.pathTargetEntryId}.`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return sections.join("\n\n");
|
|
400
|
+
}
|