@astrosheep/pi-context 0.16.0 → 0.17.0
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/package.json +1 -1
- package/src/history-tools.ts +6 -0
- package/src/history.ts +15 -3
- package/src/note-tools.ts +9 -0
- package/src/tool-schema.ts +8 -6
package/package.json
CHANGED
package/src/history-tools.ts
CHANGED
|
@@ -66,6 +66,12 @@ export function registerHistoryTools(pi: ExtensionAPI) {
|
|
|
66
66
|
async execute(_id, params, _signal, _update, ctx) {
|
|
67
67
|
const item = allItems(ctx).find((candidate) => candidate.windowId === params.window_id && candidate.itemId === params.item_id);
|
|
68
68
|
if (!item) return output({ error: "unknown item_id or window_id" });
|
|
69
|
+
const totalChars = Array.from(item.content).length;
|
|
70
|
+
// A positive offset past the end is an addressing error, not an empty page: say so,
|
|
71
|
+
// and name the largest legal offset (offset == total stays the legal empty end-read).
|
|
72
|
+
if (typeof params.offset_chars === "number" && params.offset_chars > totalChars) {
|
|
73
|
+
return output({ error: `offset_chars ${params.offset_chars} is past the end: the item has ${totalChars} chars; the largest legal offset is ${totalChars} (an empty end-read)`, window_id: item.windowId, item_id: item.itemId, offset_chars: params.offset_chars, total_chars: totalChars });
|
|
74
|
+
}
|
|
69
75
|
const limit_chars = Math.min(params.limit_chars ?? 12000, 50000);
|
|
70
76
|
return readCharacterWindow(item.content, params.offset_chars, params.limit_chars, (window) => {
|
|
71
77
|
const { content, ...cursor } = window;
|
package/src/history.ts
CHANGED
|
@@ -10,6 +10,11 @@ type HistoryItem = {
|
|
|
10
10
|
content: string;
|
|
11
11
|
createdAt: string | undefined;
|
|
12
12
|
toolName?: string;
|
|
13
|
+
// bashExecution only: the persisted output was truncated and the full text lives on disk.
|
|
14
|
+
outputTruncated?: boolean;
|
|
15
|
+
fullOutputPath?: string;
|
|
16
|
+
// toolResult only: the run reported an error.
|
|
17
|
+
toolError?: boolean;
|
|
13
18
|
};
|
|
14
19
|
type HistoryWindow = { windowId: string; createdAt?: string; items: HistoryItem[] };
|
|
15
20
|
|
|
@@ -54,10 +59,13 @@ function messageContent(message: AgentMessage): string {
|
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
function toolInfo(message: AgentMessage): Pick<HistoryItem, "toolName"> {
|
|
58
|
-
if (message.role === "bashExecution")
|
|
62
|
+
function toolInfo(message: AgentMessage): Pick<HistoryItem, "toolName" | "outputTruncated" | "fullOutputPath" | "toolError"> {
|
|
63
|
+
if (message.role === "bashExecution") {
|
|
64
|
+
// A truncated bash run is only half the record without the on-disk path: surface both.
|
|
65
|
+
return { toolName: "bash", outputTruncated: message.truncated || undefined, fullOutputPath: message.truncated ? message.fullOutputPath : undefined };
|
|
66
|
+
}
|
|
59
67
|
if (message.role !== "toolResult") return {};
|
|
60
|
-
return { toolName: message.toolName };
|
|
68
|
+
return { toolName: message.toolName, toolError: message.isError === true ? true : undefined };
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
/**
|
|
@@ -153,6 +161,10 @@ export function visibleItem(item: HistoryItem, maxChars = 1200) {
|
|
|
153
161
|
item_id: item.itemId,
|
|
154
162
|
role: item.role,
|
|
155
163
|
tool_name: item.toolName ?? null,
|
|
164
|
+
// Surfaced only when set: a truncated bash run names its full-output path, and an
|
|
165
|
+
// errored tool run says so. Absent keys mean nothing special happened.
|
|
166
|
+
...(item.outputTruncated ? { output_truncated: true, full_output_path: item.fullOutputPath ?? null } : {}),
|
|
167
|
+
...(item.toolError ? { tool_error: true } : {}),
|
|
156
168
|
truncated,
|
|
157
169
|
total_chars: characters.length,
|
|
158
170
|
// A truncated payload is a plain prefix: no synthetic marker is appended, and
|
package/src/note-tools.ts
CHANGED
|
@@ -54,6 +54,12 @@ export function registerNoteTools(pi: ExtensionAPI) {
|
|
|
54
54
|
const path = assertVirtualPath(params.path);
|
|
55
55
|
const file = notesFromSession(ctx).get(path);
|
|
56
56
|
if (!file) return output({ error: "note file not found", path });
|
|
57
|
+
const totalChars = Array.from(file.text).length;
|
|
58
|
+
// A positive offset past the end is an addressing error, not an empty page: say so,
|
|
59
|
+
// and name the largest legal offset (offset == total stays the legal empty end-read).
|
|
60
|
+
if (typeof params.offset_chars === "number" && params.offset_chars > totalChars) {
|
|
61
|
+
return output({ error: `offset_chars ${params.offset_chars} is past the end: the note has ${totalChars} chars; the largest legal offset is ${totalChars} (an empty end-read)`, path, offset_chars: params.offset_chars, total_chars: totalChars });
|
|
62
|
+
}
|
|
57
63
|
const created_at = localIso(file.createdAt);
|
|
58
64
|
const updated_at = localIso(file.updatedAt);
|
|
59
65
|
const limit_chars = Math.min(params.limit_chars ?? 12000, 50000);
|
|
@@ -146,6 +152,9 @@ export function registerNoteTools(pi: ExtensionAPI) {
|
|
|
146
152
|
if (!hasText && !hasStale) return output({ error: "provide text, mark_stale, or both", path });
|
|
147
153
|
const old = notesFromSession(ctx).get(path);
|
|
148
154
|
if (!hasText && !old) return output({ error: "note file not found", path });
|
|
155
|
+
// Appending is not creating: an append to a path that does not exist almost always
|
|
156
|
+
// means a typo'd path, so it dies loudly instead of silently minting a new note.
|
|
157
|
+
if (op === "append" && hasText && !old) return output({ error: "note file not found (use notes_write_file to create)", path });
|
|
149
158
|
const next = hasText ? (op === "append" ? `${old?.text ?? ""}${params.text}` : params.text as string) : old!.text;
|
|
150
159
|
const bytes = Buffer.byteLength(next, "utf8");
|
|
151
160
|
if (hasText && bytes > MAX_NOTE_BYTES) return output({ error: `note exceeds ${MAX_NOTE_BYTES} UTF-8 bytes`, path, size_bytes: bytes });
|
package/src/tool-schema.ts
CHANGED
|
@@ -12,13 +12,15 @@ export const searchQuery = () => Type.Union([Type.String(), Type.Array(Type.Stri
|
|
|
12
12
|
/**
|
|
13
13
|
* Normalize a search `query` parameter into the literal needles to match.
|
|
14
14
|
* A bare string is a one-element list, so single-query behavior is unchanged.
|
|
15
|
-
* An empty list
|
|
16
|
-
* for nothing:
|
|
15
|
+
* An empty list, a non-string element, or an empty string is refused rather than silently
|
|
16
|
+
* searching for nothing: those are argument errors, not empty result sets. An empty string
|
|
17
|
+
* matches every line and every item, so it can never be what the caller meant.
|
|
17
18
|
*/
|
|
18
19
|
export function searchQueries(query: unknown): string[] {
|
|
19
|
-
|
|
20
|
-
if (!Array.isArray(
|
|
21
|
-
if (!
|
|
22
|
-
|
|
20
|
+
const candidates = typeof query === "string" ? [query] : query;
|
|
21
|
+
if (!Array.isArray(candidates) || candidates.length === 0) throw new Error("query must be a string or a non-empty array of strings");
|
|
22
|
+
if (!candidates.every((candidate) => typeof candidate === "string")) throw new Error("query array elements must be strings");
|
|
23
|
+
if (candidates.some((candidate) => candidate === "")) throw new Error("query strings must be non-empty: an empty query matches everything");
|
|
24
|
+
return candidates as string[];
|
|
23
25
|
}
|
|
24
26
|
|