@nklisch/pi-enhanced 0.3.0 → 0.3.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/CHANGELOG.md +6 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +2 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +12 -5
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query.ts +229 -68
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/query-session-tool.ts +115 -33
- package/node_modules/@nklisch/pi-plugins/package.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
Binary file
|
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [18.2.0-nklisch.1] - 2026-09-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Search complete transcript message, tool-argument, tool-name, call-id, and result fields instead of applying the returned-preview cap before matching.
|
|
13
|
+
- Return bounded context around the actual matching passage with explicit full-search and source-range metadata.
|
|
14
|
+
- Add stateless offset pagination with truthful next/previous offsets, distinct out-of-range outcomes, and guaranteed forward progress when byte or line bounds shorten a page.
|
|
15
|
+
|
|
8
16
|
## [18.2.0-nklisch.0] - 2026-09-01
|
|
9
17
|
|
|
10
18
|
### Added
|
|
@@ -236,8 +236,9 @@ Search a child transcript without steering or changing the child. The tool reads
|
|
|
236
236
|
| `kind` | `all`, `messages`, `tool_calls`, or `tool_results` | no | Search scope; defaults to `all` |
|
|
237
237
|
| `order` | `newest` or `oldest` | no | Result order; defaults to `newest` |
|
|
238
238
|
| `limit` | integer 1–50 | no | Maximum entries; defaults to 20 |
|
|
239
|
+
| `offset` | non-negative safe integer | no | Offset into the ordered matching entries; defaults to 0 |
|
|
239
240
|
|
|
240
|
-
|
|
241
|
+
Matching is complete and case-insensitive literal across visible message text, tool names and IDs, arguments, and correlated results. Returned excerpts are bounded and center the first match with omission markers and source-range metadata; an omitted or empty query keeps beginning-of-entry previews. Tool names and call IDs are searched in full but exposed as bounded display previews, while stable entry IDs remain intact for overlay correlation. Output is also byte- and line-bounded, and every non-empty matching page emits at least one bounded entry. Details report the total, returned, before, and after counts plus copyable next/previous offsets when applicable. Use the next offset only from the returned details; it advances by entries actually emitted when output bounds shorten a page and is always greater than the requested offset. A zero-match query reports `no_matches`, while an offset beyond a non-empty result set reports `page_out_of_range`. Results identify the source and retained transcript path when one exists; use `/subagents:sessions` for the complete native transcript and `get_subagent_result` for status or final output.
|
|
241
242
|
|
|
242
243
|
### `steer_subagent`
|
|
243
244
|
|
|
@@ -365,11 +365,18 @@ src/
|
|
|
365
365
|
### Transcript query boundary
|
|
366
366
|
|
|
367
367
|
`query_subagent_session` is a parent-only, read-only pull surface. Its pure
|
|
368
|
-
`session/query.ts` projection correlates tool results to calls,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
368
|
+
`session/query.ts` projection correlates tool results to calls, searches every
|
|
369
|
+
complete visible field with literal case-insensitive matching, and returns
|
|
370
|
+
match-centered bounded excerpts with source-range metadata. Tool names and call
|
|
371
|
+
IDs remain complete only in the projection-local search/correlation data;
|
|
372
|
+
returned display values are bounded while stable entry IDs remain intact for the
|
|
373
|
+
overlay. Scope, ordering, 1–50 result limits, and stateless numeric offsets page
|
|
374
|
+
the ordered matching set; byte and line output bounds may shorten a page without
|
|
375
|
+
advancing beyond entries actually returned, but a non-empty page always emits a
|
|
376
|
+
bounded first entry. It reads a live child session when retained and otherwise
|
|
377
|
+
uses the same Pi JSONL/context adapter as native session navigation. No index,
|
|
378
|
+
cursor, or query state is persisted, and successful reads report complete
|
|
379
|
+
search rather than a partial-search state.
|
|
373
380
|
|
|
374
381
|
`/subagents:sessions` keeps Pi's native message, markdown, and tool-execution
|
|
375
382
|
components for the transcript body. Its overlay owns only ephemeral search
|
package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query.ts
CHANGED
|
@@ -13,24 +13,33 @@ export type SessionQueryOrder = "newest" | "oldest";
|
|
|
13
13
|
/** Overlay family filter; tool query parameters continue to use `kind`. */
|
|
14
14
|
export type SessionQueryEntryFamily = "all" | "tools";
|
|
15
15
|
export type ToolCallState = "pending" | "completed" | "failed";
|
|
16
|
+
export type SessionQueryField = "text" | "toolName" | "toolCallId" | "arguments" | "result";
|
|
17
|
+
export type SessionQueryOutcome = "matches" | "no_matches" | "page_out_of_range";
|
|
16
18
|
|
|
17
19
|
export const DEFAULT_QUERY_LIMIT = 20;
|
|
18
20
|
export const MIN_QUERY_LIMIT = 1;
|
|
19
21
|
export const MAX_QUERY_LIMIT = 50;
|
|
20
22
|
|
|
21
|
-
/**
|
|
22
|
-
export const QUERY_SEARCH_FIELD_CAP = 8_192;
|
|
23
|
+
/** Returned message text is bounded; matching always uses its complete source field. */
|
|
23
24
|
export const MESSAGE_EXCERPT_CAP = 2_000;
|
|
24
25
|
export const TOOL_ARGUMENTS_EXCERPT_CAP = 2_000;
|
|
25
26
|
export const TOOL_RESULT_EXCERPT_CAP = 4_000;
|
|
26
27
|
|
|
27
28
|
export interface QueryTruncation {
|
|
28
|
-
/** Search fields capped before matching, by field name. */
|
|
29
|
-
readonly searchFields: readonly string[];
|
|
30
29
|
/** Returned excerpts capped for the caller, by field name. */
|
|
31
30
|
readonly excerpts: readonly string[];
|
|
32
31
|
}
|
|
33
32
|
|
|
33
|
+
export interface QueryMatch {
|
|
34
|
+
/** The complete source field containing the first match. */
|
|
35
|
+
readonly field: SessionQueryField;
|
|
36
|
+
/** UTF-16 offsets in the complete, unmarked source field. */
|
|
37
|
+
readonly sourceRange: {
|
|
38
|
+
readonly start: number;
|
|
39
|
+
readonly end: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
interface QueryEntryBase {
|
|
35
44
|
/** Stable only within this projection; it is never written to a session. */
|
|
36
45
|
readonly id: string;
|
|
@@ -38,21 +47,24 @@ interface QueryEntryBase {
|
|
|
38
47
|
readonly sourceIndex: number;
|
|
39
48
|
readonly timestamp?: number;
|
|
40
49
|
readonly truncation: QueryTruncation;
|
|
50
|
+
/** Present only for a non-empty query; absent for prefix browsing. */
|
|
51
|
+
readonly match?: QueryMatch;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export interface MessageQueryEntry extends QueryEntryBase {
|
|
44
55
|
readonly kind: "message";
|
|
45
56
|
readonly role: "user" | "assistant";
|
|
46
|
-
/**
|
|
57
|
+
/** A prefix preview for browsing, or a match-centered excerpt for a query. */
|
|
47
58
|
readonly text: string;
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
export interface ToolCallQueryEntry extends QueryEntryBase {
|
|
51
62
|
readonly kind: "tool_call";
|
|
52
|
-
/**
|
|
63
|
+
/** Bounded display preview; undefined for native bash-execution messages. */
|
|
53
64
|
readonly toolCallId?: string;
|
|
65
|
+
/** Bounded display preview; the complete name remains query-local. */
|
|
54
66
|
readonly toolName: string;
|
|
55
|
-
/**
|
|
67
|
+
/** A bounded JSON arguments preview, or a match-centered excerpt for a query. */
|
|
56
68
|
readonly arguments: string;
|
|
57
69
|
readonly state: ToolCallState;
|
|
58
70
|
/** Bounded result text; undefined means pending/no correlated result, while `""` is a completed empty result. */
|
|
@@ -68,20 +80,36 @@ export interface SessionQueryOptions {
|
|
|
68
80
|
readonly limit?: number;
|
|
69
81
|
/** Restrict the result to the overlay's tool family without changing search fields. */
|
|
70
82
|
readonly entryFamily?: SessionQueryEntryFamily;
|
|
83
|
+
/** Ordered matching-entry offset; no cursor or query state is retained. */
|
|
84
|
+
readonly offset?: number;
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
export interface SessionQueryResult {
|
|
74
88
|
readonly entries: readonly SessionQueryEntry[];
|
|
75
|
-
/** Number of entries
|
|
89
|
+
/** Number of matching entries before paging. */
|
|
76
90
|
readonly totalMatches: number;
|
|
77
|
-
|
|
91
|
+
/** Offset applied to the ordered matching-entry set. */
|
|
92
|
+
readonly offset: number;
|
|
93
|
+
/** Number of entries returned by this model page. */
|
|
94
|
+
readonly returnedCount: number;
|
|
95
|
+
/** Offset for the next page, when entries remain after this page. */
|
|
96
|
+
readonly nextOffset?: number;
|
|
97
|
+
/** Offset for a preceding page, when this result has a preceding page. */
|
|
98
|
+
readonly previousOffset?: number;
|
|
99
|
+
/** Matching entries before the returned page. */
|
|
100
|
+
readonly omittedBefore: number;
|
|
101
|
+
/** Matching entries after the returned page. */
|
|
102
|
+
readonly omittedAfter: number;
|
|
78
103
|
readonly hasMore: boolean;
|
|
104
|
+
/** True whenever the transcript source was read successfully. */
|
|
105
|
+
readonly searchComplete: true;
|
|
106
|
+
readonly outcome: SessionQueryOutcome;
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
interface SearchableEntry {
|
|
82
110
|
entry: SessionQueryEntry;
|
|
83
|
-
searchable
|
|
84
|
-
|
|
111
|
+
/** Complete raw searchable fields; this object never escapes the query call. */
|
|
112
|
+
searchable: Readonly<Partial<Record<SessionQueryField, string>>>;
|
|
85
113
|
}
|
|
86
114
|
|
|
87
115
|
interface BoundedText {
|
|
@@ -104,8 +132,7 @@ function projectSearchableMessages(messages: readonly SessionMessage[]): Searcha
|
|
|
104
132
|
const text = visibleText(message.content);
|
|
105
133
|
if (text.trim()) {
|
|
106
134
|
const bounded = bound(text, MESSAGE_EXCERPT_CAP);
|
|
107
|
-
const
|
|
108
|
-
const searchable = searchFields(rawSearchable);
|
|
135
|
+
const searchable = { text };
|
|
109
136
|
projected.push({
|
|
110
137
|
entry: {
|
|
111
138
|
kind: "message",
|
|
@@ -114,10 +141,9 @@ function projectSearchableMessages(messages: readonly SessionMessage[]): Searcha
|
|
|
114
141
|
...(timestampOf(message) === undefined ? {} : { timestamp: timestampOf(message) }),
|
|
115
142
|
role: message.role,
|
|
116
143
|
text: bounded.value,
|
|
117
|
-
truncation: truncationFor(
|
|
144
|
+
truncation: truncationFor({ text: bounded }),
|
|
118
145
|
},
|
|
119
146
|
searchable,
|
|
120
|
-
rawSearchable,
|
|
121
147
|
});
|
|
122
148
|
}
|
|
123
149
|
if (message.role === "assistant") {
|
|
@@ -125,23 +151,30 @@ function projectSearchableMessages(messages: readonly SessionMessage[]): Searcha
|
|
|
125
151
|
if (content.type !== "toolCall") continue;
|
|
126
152
|
const callId = content.id;
|
|
127
153
|
const args = stringifyArguments(content.arguments);
|
|
154
|
+
const boundedToolName = boundPreview(content.name, TOOL_ARGUMENTS_EXCERPT_CAP);
|
|
155
|
+
const boundedToolCallId = boundPreview(callId, TOOL_ARGUMENTS_EXCERPT_CAP);
|
|
128
156
|
const boundedArgs = bound(args, TOOL_ARGUMENTS_EXCERPT_CAP);
|
|
129
|
-
|
|
130
|
-
|
|
157
|
+
// Keep the complete name and id only in this projection-local search
|
|
158
|
+
// record. The public entry carries bounded display values, while its
|
|
159
|
+
// stable `id` remains the full correlation key used by the overlay.
|
|
160
|
+
const searchable = { toolName: content.name, toolCallId: callId, arguments: args };
|
|
131
161
|
const call: SearchableEntry = {
|
|
132
162
|
entry: {
|
|
133
163
|
kind: "tool_call",
|
|
134
164
|
id: callId,
|
|
135
165
|
sourceIndex,
|
|
136
166
|
...(timestampOf(message) === undefined ? {} : { timestamp: timestampOf(message) }),
|
|
137
|
-
toolCallId:
|
|
138
|
-
toolName:
|
|
167
|
+
toolCallId: boundedToolCallId.value,
|
|
168
|
+
toolName: boundedToolName.value,
|
|
139
169
|
arguments: boundedArgs.value,
|
|
140
170
|
state: "pending",
|
|
141
|
-
truncation: truncationFor(
|
|
171
|
+
truncation: truncationFor({
|
|
172
|
+
toolName: boundedToolName,
|
|
173
|
+
toolCallId: boundedToolCallId,
|
|
174
|
+
arguments: boundedArgs,
|
|
175
|
+
}),
|
|
142
176
|
},
|
|
143
177
|
searchable,
|
|
144
|
-
rawSearchable,
|
|
145
178
|
};
|
|
146
179
|
projected.push(call);
|
|
147
180
|
calls.set(callId, call);
|
|
@@ -163,8 +196,7 @@ function projectSearchableMessages(messages: readonly SessionMessage[]): Searcha
|
|
|
163
196
|
const result = state === "pending" ? undefined : message.output ?? "";
|
|
164
197
|
const boundedArgs = bound(args, TOOL_ARGUMENTS_EXCERPT_CAP);
|
|
165
198
|
const boundedResult = result === undefined ? undefined : bound(result, TOOL_RESULT_EXCERPT_CAP);
|
|
166
|
-
const
|
|
167
|
-
const searchable = searchFields(rawSearchable);
|
|
199
|
+
const searchable = { toolName: "bash", arguments: args, ...(result === undefined ? {} : { result }) };
|
|
168
200
|
projected.push({
|
|
169
201
|
entry: {
|
|
170
202
|
kind: "tool_call",
|
|
@@ -175,13 +207,12 @@ function projectSearchableMessages(messages: readonly SessionMessage[]): Searcha
|
|
|
175
207
|
arguments: boundedArgs.value,
|
|
176
208
|
state,
|
|
177
209
|
...(boundedResult === undefined ? {} : { result: boundedResult.value }),
|
|
178
|
-
truncation: truncationFor(
|
|
210
|
+
truncation: truncationFor({
|
|
179
211
|
arguments: boundedArgs,
|
|
180
212
|
...(boundedResult === undefined ? {} : { result: boundedResult }),
|
|
181
213
|
}),
|
|
182
214
|
},
|
|
183
215
|
searchable,
|
|
184
|
-
rawSearchable,
|
|
185
216
|
});
|
|
186
217
|
}
|
|
187
218
|
}
|
|
@@ -195,22 +226,20 @@ function projectSearchableMessages(messages: readonly SessionMessage[]): Searcha
|
|
|
195
226
|
const current = call.entry as ToolCallQueryEntry;
|
|
196
227
|
const resultText = visibleText(message.content);
|
|
197
228
|
const boundedResult = bound(resultText, TOOL_RESULT_EXCERPT_CAP);
|
|
198
|
-
const
|
|
199
|
-
const searchable = searchFields(rawSearchable);
|
|
229
|
+
const searchable = { ...call.searchable, result: resultText };
|
|
200
230
|
const updated: ToolCallQueryEntry = {
|
|
201
231
|
...current,
|
|
202
232
|
state: message.isError ? "failed" : "completed",
|
|
203
233
|
// Preserve an empty but completed result as `""`; undefined means that
|
|
204
234
|
// no correlated result has arrived yet.
|
|
205
235
|
result: boundedResult.value,
|
|
206
|
-
truncation: truncationFor(
|
|
207
|
-
arguments: bound(call.
|
|
236
|
+
truncation: truncationFor({
|
|
237
|
+
arguments: bound(call.searchable.arguments ?? current.arguments, TOOL_ARGUMENTS_EXCERPT_CAP),
|
|
208
238
|
result: boundedResult,
|
|
209
239
|
}),
|
|
210
240
|
};
|
|
211
241
|
call.entry = updated;
|
|
212
242
|
call.searchable = searchable;
|
|
213
|
-
call.rawSearchable = rawSearchable;
|
|
214
243
|
}
|
|
215
244
|
|
|
216
245
|
return projected;
|
|
@@ -222,23 +251,50 @@ export function querySession(
|
|
|
222
251
|
options: SessionQueryOptions = {},
|
|
223
252
|
): SessionQueryResult {
|
|
224
253
|
const limit = normalizeLimit(options.limit);
|
|
254
|
+
const offset = normalizeOffset(options.offset);
|
|
225
255
|
const kind = options.kind ?? "all";
|
|
226
256
|
const order = options.order ?? "newest";
|
|
227
257
|
const query = options.query ?? "";
|
|
228
258
|
const needle = query.toLowerCase();
|
|
229
259
|
const projected = projectSearchableMessages(messages);
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
.
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
|
|
260
|
+
const matching = projected.flatMap((candidate): SessionQueryEntry[] => {
|
|
261
|
+
if (options.entryFamily === "tools" && candidate.entry.kind !== "tool_call") return [];
|
|
262
|
+
if (!matchesKind(candidate.entry, kind)) return [];
|
|
263
|
+
if (needle.length === 0) return [candidate.entry];
|
|
264
|
+
const match = findFirstMatch(candidate.entry, candidate.searchable, needle, kind);
|
|
265
|
+
return match === undefined ? [] : [entryWithMatch(candidate, match)];
|
|
266
|
+
});
|
|
267
|
+
const ordered = order === "newest" ? [...matching].reverse() : matching;
|
|
268
|
+
const totalMatches = ordered.length;
|
|
269
|
+
const outcome: SessionQueryOutcome = totalMatches === 0
|
|
270
|
+
? "no_matches"
|
|
271
|
+
: offset >= totalMatches
|
|
272
|
+
? "page_out_of_range"
|
|
273
|
+
: "matches";
|
|
274
|
+
const entries = outcome === "matches"
|
|
275
|
+
? ordered.slice(offset, Math.min(totalMatches, offset + limit))
|
|
276
|
+
: [];
|
|
277
|
+
const returnedCount = entries.length;
|
|
278
|
+
const nextOffset = offset < totalMatches && offset + returnedCount < totalMatches
|
|
279
|
+
? offset + returnedCount
|
|
280
|
+
: undefined;
|
|
281
|
+
const previousOffset = totalMatches > 0 && offset > 0
|
|
282
|
+
? Math.max(0, Math.min(Math.max(0, totalMatches - limit), offset - limit))
|
|
283
|
+
: undefined;
|
|
284
|
+
const omittedBefore = Math.min(offset, totalMatches);
|
|
285
|
+
const omittedAfter = Math.max(0, totalMatches - offset - returnedCount);
|
|
237
286
|
return {
|
|
238
287
|
entries,
|
|
239
|
-
totalMatches
|
|
240
|
-
|
|
241
|
-
|
|
288
|
+
totalMatches,
|
|
289
|
+
offset,
|
|
290
|
+
returnedCount,
|
|
291
|
+
...(nextOffset === undefined ? {} : { nextOffset }),
|
|
292
|
+
...(previousOffset === undefined ? {} : { previousOffset }),
|
|
293
|
+
omittedBefore,
|
|
294
|
+
omittedAfter,
|
|
295
|
+
hasMore: nextOffset !== undefined,
|
|
296
|
+
searchComplete: true,
|
|
297
|
+
outcome,
|
|
242
298
|
};
|
|
243
299
|
}
|
|
244
300
|
|
|
@@ -250,6 +306,14 @@ export function normalizeLimit(limit: number | undefined): number {
|
|
|
250
306
|
return value;
|
|
251
307
|
}
|
|
252
308
|
|
|
309
|
+
export function normalizeOffset(offset: number | undefined): number {
|
|
310
|
+
const value = offset ?? 0;
|
|
311
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
312
|
+
throw new RangeError("offset must be a non-negative safe integer");
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
|
|
253
317
|
function matchesKind(entry: SessionQueryEntry, kind: SessionQueryKind): boolean {
|
|
254
318
|
if (kind === "all") return true;
|
|
255
319
|
if (kind === "messages") return entry.kind === "message";
|
|
@@ -257,20 +321,74 @@ function matchesKind(entry: SessionQueryEntry, kind: SessionQueryKind): boolean
|
|
|
257
321
|
return entry.kind === "tool_call" && entry.result !== undefined;
|
|
258
322
|
}
|
|
259
323
|
|
|
260
|
-
function
|
|
261
|
-
|
|
324
|
+
function findFirstMatch(
|
|
325
|
+
entry: SessionQueryEntry,
|
|
326
|
+
searchable: Readonly<Partial<Record<SessionQueryField, string>>>,
|
|
262
327
|
needle: string,
|
|
263
328
|
kind: SessionQueryKind,
|
|
264
|
-
):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
329
|
+
): QueryMatch | undefined {
|
|
330
|
+
const fields = entry.kind === "message"
|
|
331
|
+
? ["text"] as const
|
|
332
|
+
: kind === "tool_results"
|
|
333
|
+
? ["result"] as const
|
|
334
|
+
: kind === "tool_calls"
|
|
335
|
+
? ["toolName", "toolCallId", "arguments"] as const
|
|
336
|
+
: ["toolName", "toolCallId", "arguments", "result"] as const;
|
|
337
|
+
for (const field of fields) {
|
|
338
|
+
const value = searchable[field];
|
|
339
|
+
if (value === undefined) continue;
|
|
340
|
+
const lowered = value.toLowerCase();
|
|
341
|
+
const matchStart = lowered.indexOf(needle);
|
|
342
|
+
if (matchStart < 0) continue;
|
|
343
|
+
const matchEnd = matchStart + needle.length;
|
|
344
|
+
const sourceStart = sourceOffsetForLower(value, matchStart, false);
|
|
345
|
+
const sourceEnd = Math.max(sourceStart + 1, sourceOffsetForLower(value, matchEnd, true));
|
|
346
|
+
return { field, sourceRange: { start: sourceStart, end: Math.min(value.length, sourceEnd) } };
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function entryWithMatch(candidate: SearchableEntry, match: QueryMatch): SessionQueryEntry {
|
|
352
|
+
const { entry, searchable } = candidate;
|
|
353
|
+
if (entry.kind === "message") {
|
|
354
|
+
const text = searchable.text ?? entry.text;
|
|
355
|
+
const bounded = boundAroundMatch(text, match.sourceRange, MESSAGE_EXCERPT_CAP);
|
|
356
|
+
return { ...entry, text: bounded.value, truncation: truncationFor({ text: bounded }), match };
|
|
271
357
|
}
|
|
272
|
-
|
|
273
|
-
|
|
358
|
+
|
|
359
|
+
const toolName = searchable.toolName ?? entry.toolName;
|
|
360
|
+
const boundedToolName = match.field === "toolName"
|
|
361
|
+
? boundAroundMatch(toolName, match.sourceRange, TOOL_ARGUMENTS_EXCERPT_CAP)
|
|
362
|
+
: boundPreview(toolName, TOOL_ARGUMENTS_EXCERPT_CAP);
|
|
363
|
+
const toolCallId = searchable.toolCallId;
|
|
364
|
+
const boundedToolCallId = toolCallId === undefined
|
|
365
|
+
? undefined
|
|
366
|
+
: match.field === "toolCallId"
|
|
367
|
+
? boundAroundMatch(toolCallId, match.sourceRange, TOOL_ARGUMENTS_EXCERPT_CAP)
|
|
368
|
+
: boundPreview(toolCallId, TOOL_ARGUMENTS_EXCERPT_CAP);
|
|
369
|
+
const argumentsText = searchable.arguments ?? entry.arguments;
|
|
370
|
+
const boundedArguments = match.field === "arguments"
|
|
371
|
+
? boundAroundMatch(argumentsText, match.sourceRange, TOOL_ARGUMENTS_EXCERPT_CAP)
|
|
372
|
+
: bound(argumentsText, TOOL_ARGUMENTS_EXCERPT_CAP);
|
|
373
|
+
const boundedResult = searchable.result === undefined
|
|
374
|
+
? undefined
|
|
375
|
+
: match.field === "result"
|
|
376
|
+
? boundAroundMatch(searchable.result, match.sourceRange, TOOL_RESULT_EXCERPT_CAP)
|
|
377
|
+
: bound(searchable.result, TOOL_RESULT_EXCERPT_CAP);
|
|
378
|
+
return {
|
|
379
|
+
...entry,
|
|
380
|
+
toolName: boundedToolName.value,
|
|
381
|
+
...(boundedToolCallId === undefined ? {} : { toolCallId: boundedToolCallId.value }),
|
|
382
|
+
arguments: boundedArguments.value,
|
|
383
|
+
...(boundedResult === undefined ? {} : { result: boundedResult.value }),
|
|
384
|
+
truncation: truncationFor({
|
|
385
|
+
toolName: boundedToolName,
|
|
386
|
+
...(boundedToolCallId === undefined ? {} : { toolCallId: boundedToolCallId }),
|
|
387
|
+
arguments: boundedArguments,
|
|
388
|
+
...(boundedResult === undefined ? {} : { result: boundedResult }),
|
|
389
|
+
}),
|
|
390
|
+
match,
|
|
391
|
+
};
|
|
274
392
|
}
|
|
275
393
|
|
|
276
394
|
function visibleText(content: unknown): string {
|
|
@@ -301,27 +419,70 @@ function messageIdentity(role: "user" | "assistant", sourceIndex: number, timest
|
|
|
301
419
|
return `message:${role}:${sourceIndex}${typeof timestamp === "number" ? `:${timestamp}` : ""}`;
|
|
302
420
|
}
|
|
303
421
|
|
|
304
|
-
function
|
|
305
|
-
return value.length >
|
|
422
|
+
function bound(value: string, cap: number): BoundedText {
|
|
423
|
+
return value.length > cap ? { value: value.slice(0, cap), truncated: true } : { value, truncated: false };
|
|
306
424
|
}
|
|
307
425
|
|
|
308
|
-
|
|
309
|
-
|
|
426
|
+
/** Prefix previews identify omitted metadata without exposing the full field. */
|
|
427
|
+
function boundPreview(value: string, cap: number): BoundedText {
|
|
428
|
+
if (value.length <= cap) return { value, truncated: false };
|
|
429
|
+
const marker = "…";
|
|
430
|
+
const contentCap = Math.max(0, cap - marker.length);
|
|
431
|
+
return { value: `${value.slice(0, contentCap)}${marker}`, truncated: true };
|
|
310
432
|
}
|
|
311
433
|
|
|
312
|
-
|
|
313
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Keep the first match visible while spending the excerpt budget on context.
|
|
436
|
+
* The source range remains in raw-field coordinates; markers are presentation
|
|
437
|
+
* text and are not included in that range.
|
|
438
|
+
*/
|
|
439
|
+
function boundAroundMatch(value: string, sourceRange: { start: number; end: number }, cap: number): BoundedText {
|
|
440
|
+
if (value.length <= cap) return { value, truncated: false };
|
|
441
|
+
const contentCap = Math.max(1, cap - 2);
|
|
442
|
+
const matchStart = Math.max(0, Math.min(value.length, sourceRange.start));
|
|
443
|
+
const matchEnd = Math.max(matchStart + 1, Math.min(value.length, sourceRange.end));
|
|
444
|
+
if (matchEnd - matchStart >= contentCap) {
|
|
445
|
+
const end = Math.min(value.length, matchStart + contentCap);
|
|
446
|
+
return { value: `${value.slice(0, matchStart) ? "…" : ""}${value.slice(matchStart, end)}${end < value.length ? "…" : ""}`, truncated: true };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const windowStart = Math.max(0, matchEnd - contentCap);
|
|
450
|
+
const windowEnd = Math.min(matchStart, value.length - contentCap);
|
|
451
|
+
const centeredStart = matchStart - Math.floor((contentCap - (matchEnd - matchStart)) / 2);
|
|
452
|
+
const start = Math.max(windowStart, Math.min(windowEnd, centeredStart));
|
|
453
|
+
const end = Math.min(value.length, start + contentCap);
|
|
454
|
+
return {
|
|
455
|
+
value: `${start > 0 ? "…" : ""}${value.slice(start, end)}${end < value.length ? "…" : ""}`,
|
|
456
|
+
truncated: true,
|
|
457
|
+
};
|
|
314
458
|
}
|
|
315
459
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Map an offset in the lower-cased field back to the original UTF-16 field.
|
|
462
|
+
* Matching itself uses `value.toLowerCase()` as a whole; this linear walk only
|
|
463
|
+
* accounts for each code point's lower-case UTF-16 width, including expanding
|
|
464
|
+
* mappings such as `İ` → `i` plus a combining dot.
|
|
465
|
+
*/
|
|
466
|
+
function sourceOffsetForLower(value: string, lowerOffset: number, end: boolean): number {
|
|
467
|
+
let sourceOffset = 0;
|
|
468
|
+
let lowerPosition = 0;
|
|
469
|
+
for (const character of value) {
|
|
470
|
+
if (lowerOffset <= lowerPosition) return sourceOffset;
|
|
471
|
+
const lowerLength = character.toLowerCase().length;
|
|
472
|
+
const nextLowerPosition = lowerPosition + lowerLength;
|
|
473
|
+
if (lowerOffset < nextLowerPosition || (end && lowerOffset === nextLowerPosition)) {
|
|
474
|
+
return end ? sourceOffset + character.length : sourceOffset;
|
|
475
|
+
}
|
|
476
|
+
sourceOffset += character.length;
|
|
477
|
+
lowerPosition = nextLowerPosition;
|
|
478
|
+
}
|
|
479
|
+
return sourceOffset;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function truncationFor(excerpts: Readonly<Record<string, BoundedText>>): QueryTruncation {
|
|
483
|
+
return {
|
|
484
|
+
excerpts: Object.entries(excerpts)
|
|
485
|
+
.filter(([, value]) => value.truncated)
|
|
486
|
+
.map(([name]) => name),
|
|
487
|
+
};
|
|
327
488
|
}
|
|
@@ -19,7 +19,7 @@ const MAX_QUERY_OUTPUT_LINES = 2_000;
|
|
|
19
19
|
// Leave room for the summary, source path, omission tail, and a final line/
|
|
20
20
|
// byte boundary. The entry budget is intentionally smaller than Pi's limits;
|
|
21
21
|
// entries are additionally checked against the actual fixed text below.
|
|
22
|
-
const MAX_QUERY_ENTRY_BYTES =
|
|
22
|
+
const MAX_QUERY_ENTRY_BYTES = 39 * 1024;
|
|
23
23
|
const MAX_QUERY_ENTRY_LINES = 1_800;
|
|
24
24
|
const MAX_QUERY_METADATA_LINE_BYTES = 2_048;
|
|
25
25
|
const MAX_QUERY_TRANSCRIPT_PATH_BYTES = 1_024;
|
|
@@ -27,7 +27,7 @@ const utf8Encoder = new TextEncoder();
|
|
|
27
27
|
const VALID_KINDS = new Set<SessionQueryKind>(["all", "messages", "tool_calls", "tool_results"]);
|
|
28
28
|
const VALID_ORDERS = new Set<SessionQueryOrder>(["newest", "oldest"]);
|
|
29
29
|
|
|
30
|
-
export type QuerySessionOutcome = "matches" | "no_matches" | "transcript_unavailable" | "not_found" | "read_error";
|
|
30
|
+
export type QuerySessionOutcome = "matches" | "no_matches" | "page_out_of_range" | "transcript_unavailable" | "not_found" | "read_error";
|
|
31
31
|
|
|
32
32
|
export interface QuerySessionDetails {
|
|
33
33
|
readonly outcome: QuerySessionOutcome;
|
|
@@ -46,10 +46,17 @@ export interface QuerySessionDetails {
|
|
|
46
46
|
readonly kind?: SessionQueryKind;
|
|
47
47
|
readonly order?: SessionQueryOrder;
|
|
48
48
|
readonly limit?: number;
|
|
49
|
+
readonly offset?: number;
|
|
49
50
|
readonly entries?: readonly SessionQueryEntry[];
|
|
50
51
|
readonly totalMatches?: number;
|
|
52
|
+
readonly returnedCount?: number;
|
|
53
|
+
readonly nextOffset?: number;
|
|
54
|
+
readonly previousOffset?: number;
|
|
55
|
+
readonly omittedBefore?: number;
|
|
56
|
+
readonly omittedAfter?: number;
|
|
51
57
|
readonly hasMore?: boolean;
|
|
52
|
-
|
|
58
|
+
/** True means every searchable field was examined, not just a preview prefix. */
|
|
59
|
+
readonly searchComplete?: boolean;
|
|
53
60
|
readonly truncation?: {
|
|
54
61
|
readonly output: boolean;
|
|
55
62
|
readonly omittedMatches: number;
|
|
@@ -79,6 +86,7 @@ export class QuerySessionTool {
|
|
|
79
86
|
kind?: SessionQueryKind;
|
|
80
87
|
order?: SessionQueryOrder;
|
|
81
88
|
limit?: number;
|
|
89
|
+
offset?: number;
|
|
82
90
|
},
|
|
83
91
|
_signal: AbortSignal | undefined,
|
|
84
92
|
_onUpdate: unknown,
|
|
@@ -131,18 +139,26 @@ export class QuerySessionTool {
|
|
|
131
139
|
kind: params.kind,
|
|
132
140
|
order: params.order,
|
|
133
141
|
limit: params.limit,
|
|
142
|
+
offset: params.offset,
|
|
134
143
|
});
|
|
135
144
|
const summary = boundedOutputLine(summaryLine(base), MAX_QUERY_METADATA_LINE_BYTES);
|
|
136
145
|
const sourceDescription = boundedOutputLine(
|
|
137
146
|
`${source}${record.outputFile ? ` (${record.outputFile})` : ""}`,
|
|
138
147
|
MAX_QUERY_METADATA_LINE_BYTES,
|
|
139
148
|
);
|
|
140
|
-
const prefix = `${summary}\nSource: ${sourceDescription}\n`;
|
|
141
|
-
// Reserve the largest possible omission tail before selecting
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
const maximumOmittedCount = result.
|
|
145
|
-
const reservedTail = omissionTail(
|
|
149
|
+
const prefix = `${summary}\nSource: ${sourceDescription}\nSearch: complete (full transcript fields).\n`;
|
|
150
|
+
// Reserve the largest possible omission/navigation tail before selecting
|
|
151
|
+
// entries. This keeps a later bounds notice from pushing the body over
|
|
152
|
+
// Pi's byte or line ceiling.
|
|
153
|
+
const maximumOmittedCount = result.omittedBefore + result.omittedAfter + result.entries.length;
|
|
154
|
+
const reservedTail = omissionTail(
|
|
155
|
+
maximumOmittedCount,
|
|
156
|
+
maximumOmittedCount,
|
|
157
|
+
maximumOmittedCount,
|
|
158
|
+
result.nextOffset,
|
|
159
|
+
result.previousOffset,
|
|
160
|
+
record.outputFile,
|
|
161
|
+
);
|
|
146
162
|
const output = formatEntries(
|
|
147
163
|
result.entries,
|
|
148
164
|
Math.min(
|
|
@@ -154,8 +170,16 @@ export class QuerySessionTool {
|
|
|
154
170
|
Math.max(0, MAX_QUERY_OUTPUT_LINES - lineCount(prefix) - lineCount(reservedTail) - 1),
|
|
155
171
|
),
|
|
156
172
|
);
|
|
157
|
-
const
|
|
158
|
-
const
|
|
173
|
+
const returnedCount = output.entries.length;
|
|
174
|
+
const omittedBefore = result.omittedBefore;
|
|
175
|
+
const omittedAfter = Math.max(0, result.totalMatches - result.offset - returnedCount);
|
|
176
|
+
// Recompute this from entries that really made it into the output. If the
|
|
177
|
+
// byte/line budget drops entries from a page, the next request starts after
|
|
178
|
+
// only those entries and cannot skip an unseen match.
|
|
179
|
+
const nextOffset = result.outcome === "matches" && returnedCount > 0 && result.offset + returnedCount < result.totalMatches
|
|
180
|
+
? result.offset + returnedCount
|
|
181
|
+
: undefined;
|
|
182
|
+
const outcome = result.outcome;
|
|
159
183
|
const details: QuerySessionDetails = {
|
|
160
184
|
...base,
|
|
161
185
|
outcome,
|
|
@@ -165,22 +189,37 @@ export class QuerySessionTool {
|
|
|
165
189
|
kind: params.kind ?? "all",
|
|
166
190
|
order: params.order ?? "newest",
|
|
167
191
|
limit: params.limit ?? DEFAULT_QUERY_LIMIT,
|
|
192
|
+
offset: result.offset,
|
|
168
193
|
entries: output.entries,
|
|
169
194
|
totalMatches: result.totalMatches,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
...(
|
|
195
|
+
returnedCount,
|
|
196
|
+
...(nextOffset === undefined ? {} : { nextOffset }),
|
|
197
|
+
...(result.previousOffset === undefined ? {} : { previousOffset: result.previousOffset }),
|
|
198
|
+
omittedBefore,
|
|
199
|
+
omittedAfter,
|
|
200
|
+
hasMore: nextOffset !== undefined,
|
|
201
|
+
searchComplete: true,
|
|
202
|
+
...(output.omittedCount > 0 ? {
|
|
173
203
|
truncation: {
|
|
174
|
-
output:
|
|
175
|
-
omittedMatches: omittedCount,
|
|
204
|
+
output: true,
|
|
205
|
+
omittedMatches: output.omittedCount,
|
|
176
206
|
...(record.outputFile ? { transcriptPath: record.outputFile } : {}),
|
|
177
207
|
},
|
|
178
208
|
} : {}),
|
|
179
209
|
};
|
|
180
|
-
const body =
|
|
210
|
+
const body = outcome === "no_matches"
|
|
181
211
|
? "No transcript entries match the requested query."
|
|
182
|
-
:
|
|
183
|
-
|
|
212
|
+
: outcome === "page_out_of_range"
|
|
213
|
+
? `No transcript page starts at offset ${result.offset}; ${result.totalMatches} matching entr${result.totalMatches === 1 ? "y is" : "ies are"} available.`
|
|
214
|
+
: output.lines.join("\n");
|
|
215
|
+
const tail = omissionTail(
|
|
216
|
+
omittedBefore,
|
|
217
|
+
omittedAfter,
|
|
218
|
+
output.omittedCount,
|
|
219
|
+
nextOffset,
|
|
220
|
+
result.previousOffset,
|
|
221
|
+
record.outputFile,
|
|
222
|
+
);
|
|
184
223
|
return textResult(`${prefix}${body}${tail}`, details);
|
|
185
224
|
}
|
|
186
225
|
|
|
@@ -189,10 +228,10 @@ export class QuerySessionTool {
|
|
|
189
228
|
name: "query_subagent_session" as const,
|
|
190
229
|
label: "Query Subagent Session",
|
|
191
230
|
promptSnippet: "query_subagent_session: Search a bounded child transcript without steering it.",
|
|
192
|
-
description: "Query a subagent's recent or matching messages and tool calls. Search is case-insensitive literal text, bounded, and read-only; use get_subagent_result for the final result and list_subagents for fleet triage.",
|
|
231
|
+
description: "Query a subagent's recent or matching messages and tool calls. Search is complete, case-insensitive literal text over full fields, bounded only in returned excerpts/output, and read-only; use get_subagent_result for the final result and list_subagents for fleet triage.",
|
|
193
232
|
parameters: Type.Object({
|
|
194
233
|
agent_id: Type.String({ description: "The subagent ID to inspect." }),
|
|
195
|
-
query: Type.Optional(Type.String({ description: "Case-insensitive literal text. Omit or use an empty string for
|
|
234
|
+
query: Type.Optional(Type.String({ description: "Case-insensitive literal text. Omit or use an empty string for beginning-of-entry previews." })),
|
|
196
235
|
kind: Type.Optional(Type.Union([
|
|
197
236
|
Type.Literal("all"),
|
|
198
237
|
Type.Literal("messages"),
|
|
@@ -204,6 +243,7 @@ export class QuerySessionTool {
|
|
|
204
243
|
Type.Literal("oldest"),
|
|
205
244
|
], { description: "Result order. Defaults to newest." })),
|
|
206
245
|
limit: Type.Optional(Type.Integer({ minimum: MIN_QUERY_LIMIT, maximum: MAX_QUERY_LIMIT, description: "Maximum entries, 1-50; defaults to 20." })),
|
|
246
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "Non-negative safe integer offset into the ordered matching entries; defaults to 0." })),
|
|
207
247
|
}),
|
|
208
248
|
execute: (toolCallId: string, params: {
|
|
209
249
|
agent_id?: string;
|
|
@@ -211,6 +251,7 @@ export class QuerySessionTool {
|
|
|
211
251
|
kind?: SessionQueryKind;
|
|
212
252
|
order?: SessionQueryOrder;
|
|
213
253
|
limit?: number;
|
|
254
|
+
offset?: number;
|
|
214
255
|
}, signal: AbortSignal | undefined, onUpdate: unknown, ctx: unknown) => this.execute(toolCallId, params, signal, onUpdate, ctx),
|
|
215
256
|
});
|
|
216
257
|
}
|
|
@@ -222,6 +263,7 @@ function validateParams(params: {
|
|
|
222
263
|
kind?: SessionQueryKind;
|
|
223
264
|
order?: SessionQueryOrder;
|
|
224
265
|
limit?: number;
|
|
266
|
+
offset?: number;
|
|
225
267
|
}): string | undefined {
|
|
226
268
|
if (typeof params.agent_id !== "string" || params.agent_id.length === 0) return "agent_id must be a non-empty string";
|
|
227
269
|
if (params.query !== undefined && typeof params.query !== "string") return "query must be a string";
|
|
@@ -230,6 +272,8 @@ function validateParams(params: {
|
|
|
230
272
|
if (params.limit !== undefined && (!Number.isInteger(params.limit) || params.limit < MIN_QUERY_LIMIT || params.limit > MAX_QUERY_LIMIT)) {
|
|
231
273
|
return `limit must be an integer from ${MIN_QUERY_LIMIT} to ${MAX_QUERY_LIMIT}`;
|
|
232
274
|
}
|
|
275
|
+
if (params.offset !== undefined && !Number.isSafeInteger(params.offset)) return "offset must be a non-negative safe integer";
|
|
276
|
+
if (params.offset !== undefined && params.offset < 0) return "offset must be a non-negative safe integer";
|
|
233
277
|
return undefined;
|
|
234
278
|
}
|
|
235
279
|
|
|
@@ -277,7 +321,25 @@ function formatEntries(
|
|
|
277
321
|
const addition = selected.length === 0 ? line : `\n${line}`;
|
|
278
322
|
const additionBytes = utf8ByteLength(addition);
|
|
279
323
|
const additionLines = lineCount(addition);
|
|
280
|
-
if (bytes + additionBytes > maxBytes || lineTotal + additionLines > maxLines)
|
|
324
|
+
if (bytes + additionBytes > maxBytes || lineTotal + additionLines > maxLines) {
|
|
325
|
+
// A newline-dense first entry can exceed the line budget even though its
|
|
326
|
+
// projection is character-bounded. Compact only this fallback
|
|
327
|
+
// representation so a real match can always make progress; later pages
|
|
328
|
+
// still use the normal output bounds and the returned count remains exact.
|
|
329
|
+
if (selected.length === 0) {
|
|
330
|
+
const compactLine = formatEntry(entry, true);
|
|
331
|
+
const compactAddition = compactLine;
|
|
332
|
+
const compactBytes = utf8ByteLength(compactAddition);
|
|
333
|
+
const compactLines = lineCount(compactAddition);
|
|
334
|
+
if (compactBytes <= maxBytes && compactLines <= maxLines) {
|
|
335
|
+
selected.push(entry);
|
|
336
|
+
lines.push(compactLine);
|
|
337
|
+
bytes = compactBytes;
|
|
338
|
+
lineTotal = compactLines;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
281
343
|
selected.push(entry);
|
|
282
344
|
lines.push(line);
|
|
283
345
|
bytes += additionBytes;
|
|
@@ -286,13 +348,21 @@ function formatEntries(
|
|
|
286
348
|
return { entries: selected, lines, omittedCount: entries.length - selected.length };
|
|
287
349
|
}
|
|
288
350
|
|
|
289
|
-
function formatEntry(entry: SessionQueryEntry): string {
|
|
351
|
+
function formatEntry(entry: SessionQueryEntry, compact = false): string {
|
|
352
|
+
const display = (value: string): string => compact ? compactLineBreaks(value) : value;
|
|
353
|
+
const match = entry.match === undefined
|
|
354
|
+
? ""
|
|
355
|
+
: ` · match=${entry.match.field} [${entry.match.sourceRange.start},${entry.match.sourceRange.end})`;
|
|
290
356
|
if (entry.kind === "message") {
|
|
291
|
-
return `[${entry.role}] ${entry.id}\n${entry.text}`;
|
|
357
|
+
return `[${entry.role}] ${entry.id}${match}\n${display(entry.text)}`;
|
|
292
358
|
}
|
|
293
|
-
const
|
|
294
|
-
const result = entry.result === undefined ? "" : `\nresult: ${entry.result}`;
|
|
295
|
-
return `[tool ${entry.toolName}
|
|
359
|
+
const callIdentity = entry.toolCallId ?? entry.id;
|
|
360
|
+
const result = entry.result === undefined ? "" : `\nresult: ${display(entry.result)}`;
|
|
361
|
+
return `[tool ${display(entry.toolName)} · ${entry.state}] id=${display(callIdentity)}${match}\narguments: ${display(entry.arguments)}${result}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function compactLineBreaks(value: string): string {
|
|
365
|
+
return value.replace(/\r\n|\r|\n/g, " ");
|
|
296
366
|
}
|
|
297
367
|
|
|
298
368
|
function utf8ByteLength(value: string): number {
|
|
@@ -321,10 +391,22 @@ function boundedOutputLine(value: string, maxBytes: number): string {
|
|
|
321
391
|
return `${output}${ellipsis}`;
|
|
322
392
|
}
|
|
323
393
|
|
|
324
|
-
function omissionTail(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
394
|
+
function omissionTail(
|
|
395
|
+
omittedBefore: number,
|
|
396
|
+
omittedAfter: number,
|
|
397
|
+
outputOmitted: number,
|
|
398
|
+
nextOffset: number | undefined,
|
|
399
|
+
previousOffset: number | undefined,
|
|
400
|
+
transcriptPath: string | undefined,
|
|
401
|
+
): string {
|
|
402
|
+
const lines: string[] = [];
|
|
403
|
+
if (omittedBefore > 0) lines.push(`${omittedBefore} matching entr${omittedBefore === 1 ? "y precedes" : "ies precede"} this page.`);
|
|
404
|
+
if (omittedAfter > 0) lines.push(`${omittedAfter} matching entr${omittedAfter === 1 ? "y remains" : "ies remain"} after this page.`);
|
|
405
|
+
if (outputOmitted > 0) lines.push(`${outputOmitted} entries from this page were omitted by the output bounds.`);
|
|
406
|
+
if (nextOffset !== undefined) lines.push(`More matches available; repeat with offset: ${nextOffset}.`);
|
|
407
|
+
if (previousOffset !== undefined) lines.push(`Previous page available; repeat with offset: ${previousOffset}.`);
|
|
408
|
+
if (transcriptPath && outputOmitted > 0) {
|
|
409
|
+
lines.push(`Full transcript: ${boundedOutputLine(transcriptPath, MAX_QUERY_TRANSCRIPT_PATH_BYTES)}`);
|
|
410
|
+
}
|
|
411
|
+
return lines.length === 0 ? "" : `\n\n${lines.join("\n")}`;
|
|
330
412
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nklisch/pi-plugins",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A small filesystem-first marketplace and plugin host for Pi.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
],
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@nklisch/pi-mcp-adapter": "2.21.0-nklisch.2",
|
|
66
|
-
"@nklisch/pi-subagents": "18.2.0-nklisch.
|
|
66
|
+
"@nklisch/pi-subagents": "18.2.0-nklisch.1",
|
|
67
67
|
"jiti": "2.7.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nklisch/pi-enhanced",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Pi, enhanced — one install for nklisch's full harness: policy-gated command review, plugin marketplace, subagents, background tasks, research tools, search, model modes, and a curated UX set.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "nklisch"
|