@howaboua/pi-notes-compaction 0.0.2 → 0.0.4
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 +11 -0
- package/PCC-INTEGRATION.md +1 -1
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/bridge.ts +3 -0
- package/src/index.ts +7 -0
- package/src/lifecycle.ts +10 -7
- package/src/notes.ts +93 -98
- package/src/settings-screen.ts +1 -1
- package/src/tools.ts +59 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @howaboua/pi-notes-compaction
|
|
2
2
|
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
- Fixed note queries and manual context rollover with Codex Conversion.
|
|
6
|
+
|
|
7
|
+
- Restored negative line ranges, default limits, result ordering, metadata and complete read and search content. Help now shows concrete call syntax, and invalid arguments are reported before execution.
|
|
8
|
+
- `/compact` now honours the Normal compaction setting. With it off, Notes Compaction reuses fresh saved notes or requests a checkpoint, then opens a new context window without starting a continuation turn.
|
|
9
|
+
|
|
10
|
+
## 0.0.3
|
|
11
|
+
|
|
12
|
+
- Fixed shared agent startup after package updates when the startup changelog is present.
|
|
13
|
+
|
|
3
14
|
## 0.0.2
|
|
4
15
|
|
|
5
16
|
- Added a `/notes` settings panel for normal compaction, idle rollover, continuity status and confirmed pruning, replacing the subcommands and JSON status output.
|
package/PCC-INTEGRATION.md
CHANGED
|
@@ -35,7 +35,7 @@ Required boundaries in the current source:
|
|
|
35
35
|
|
|
36
36
|
- `context-management/tools.ts` and `extension/tools.ts`: replace the four tools through the registration callback. Active-tool and schema rewrites must preserve delegated definitions. Do not reinstall legacy routers on model changes or settings changes.
|
|
37
37
|
- `extension/session-lifecycle.ts` and `extension/turn-lifecycle.ts`: skip legacy notes/window initialization, input holding, checkpoint admission, note freshness, reminders, settlement records and continuation scheduling. Leave PCC's prompt preparation, runtime synchronization, Notebook status, voice, quota and provider lifecycle active.
|
|
38
|
-
- `extension/compaction-lifecycle.ts`:
|
|
38
|
+
- `extension/compaction-lifecycle.ts`: consult `owner.allowsCompaction(reason)` before any compaction work. When false, return without work so the owner's hook can cancel and handle notes-only rollover, regardless of extension order. Skip legacy `contextWindows.prepareCompaction` and notes checkpoint overrides. When this extension allows normal compaction, run the unchanged installed PCC normal-compaction path. Overflow stays in the same window. Notebook checkpointing, native compaction, replay, transport reset and presentation remain PCC-owned.
|
|
39
39
|
- `context-management/window-manager.ts`, `window-request.ts` and native replay serializers: use `owner.identity(ctx)` and `owner.projectBranch(entries)` for the active window and retirement boundary. Preserve PCC request metadata and zero-based generation semantics. Do not recreate remote history-ingest requirements for local notes.
|
|
40
40
|
- PCC message preparation and `adapter/developer-messages.ts`: preserve `notes-compaction:window:v1` and `notes-compaction:checkpoint:v1` custom messages on every model. Compatible Responses routes may promote them to developer messages. Ordinary Pi conversion must retain them on other routes. Use `owner.projectMessages(messages)` before native replay/prewarm, without overwriting another extension's preparation.
|
|
41
41
|
- Settings UI: show notes continuity as externally managed. Preserve old PCC values for sessions without the external extension. Do not add another compaction setting to this extension.
|
package/README.md
CHANGED
|
@@ -20,7 +20,9 @@ The agent keeps the familiar `notes`, `history`, `new_context` and `get_context_
|
|
|
20
20
|
|
|
21
21
|
By default, `new_context` starts a fresh window without generating a summary. The agent receives recent note paths, and Pi's saved history remains available for lookup.
|
|
22
22
|
|
|
23
|
-
Reminders arrive at 85% and 90% context use. `/compact`
|
|
23
|
+
Reminders arrive at 85% and 90% context use. With normal compaction off, `/compact` opens a fresh window immediately if the latest completed turn saved fresh notes. Otherwise it requests a checkpoint first. The new window waits for your next prompt; it does not start a continuation turn. Abort, errors and failed writes retain the old context. Overflow recovery still uses Pi's normal compaction.
|
|
24
|
+
|
|
25
|
+
Pi rejects `/compact` when there is too little history to prepare compaction, before extensions run. In that case, the agent can still use `new_context`.
|
|
24
26
|
|
|
25
27
|
Idle rollover is off by default. Choose 5, 15, 25 or 55 minutes in `/notes`. After that interval, the next input waits for rollover only if the latest completed turn saved fresh notes. Otherwise it proceeds in the current window without requesting a checkpoint. These intervals are user preferences, not provider cache-expiry guarantees. Held input retains its original text, attachments and SDK options. A failed rollover keeps input pending. Submit another prompt to retry, or reload to cancel it.
|
|
26
28
|
|
|
@@ -28,7 +30,7 @@ Idle rollover is off by default. Choose 5, 15, 25 or 55 minutes in `/notes`. Aft
|
|
|
28
30
|
|
|
29
31
|
`/notes` opens the settings panel in interactive Pi. It shows continuity status, storage counts and remote capabilities. Changes save immediately.
|
|
30
32
|
|
|
31
|
-
- **Normal compaction**
|
|
33
|
+
- **Normal compaction** uses the installed Pi or PCC compaction flow for `/compact` and `new_context`. Off keeps notes-only rollover, the default.
|
|
32
34
|
- **Idle rollover** selects the interval or disables it.
|
|
33
35
|
- **Prune missing sessions** removes records for missing session files after confirmation. Unknown paths, permission errors and the active session are retained.
|
|
34
36
|
|
package/package.json
CHANGED
package/src/bridge.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AgentToolResult,
|
|
4
4
|
ExtensionAPI,
|
|
5
5
|
ExtensionContext,
|
|
6
|
+
SessionBeforeCompactEvent,
|
|
6
7
|
SessionEntry,
|
|
7
8
|
ToolDefinition,
|
|
8
9
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -68,6 +69,8 @@ export interface NotesOwner {
|
|
|
68
69
|
identity(ctx: ExtensionContext): WindowIdentity | undefined;
|
|
69
70
|
projectMessages(messages: readonly AgentMessage[]): AgentMessage[];
|
|
70
71
|
projectBranch(entries: readonly SessionEntry[]): SessionEntry[];
|
|
72
|
+
/** Consult before compaction work; the owner's hook performs notes-only rollover. */
|
|
73
|
+
allowsCompaction?(reason: SessionBeforeCompactEvent["reason"]): boolean;
|
|
71
74
|
canBind?(ctx: ExtensionContext): boolean;
|
|
72
75
|
snapshotNotes?(
|
|
73
76
|
entries: readonly SessionEntry[],
|
package/src/index.ts
CHANGED
|
@@ -43,10 +43,17 @@ export default function notesCompaction(pi: ExtensionAPI): void {
|
|
|
43
43
|
lifecycle.windows.current ? { ...lifecycle.windows.current } : undefined,
|
|
44
44
|
projectMessages: (messages) => lifecycle.windows.projectMessages(messages),
|
|
45
45
|
projectBranch: (entries) => lifecycle.windows.projectBranch(entries),
|
|
46
|
+
allowsCompaction: (reason) => lifecycle.allowsCompaction(reason),
|
|
46
47
|
canBind: (ctx) =>
|
|
47
48
|
lifecycle.active &&
|
|
48
49
|
ctx.isIdle() &&
|
|
49
50
|
ctx.sessionManager.getEntries().every((entry) => {
|
|
51
|
+
// The first startup after an update records a UI-only changelog.
|
|
52
|
+
if (
|
|
53
|
+
entry.type === "custom" &&
|
|
54
|
+
entry.customType === "@howaboua/pi-stuff/changelog"
|
|
55
|
+
)
|
|
56
|
+
return true;
|
|
50
57
|
// Herdr activation persists its empty registry before first task delivery.
|
|
51
58
|
if (
|
|
52
59
|
entry.type === "custom" &&
|
package/src/lifecycle.ts
CHANGED
|
@@ -357,11 +357,7 @@ export class NotesLifecycle {
|
|
|
357
357
|
event: SessionBeforeCompactEvent,
|
|
358
358
|
ctx: ExtensionContext,
|
|
359
359
|
): { cancel: true } | undefined {
|
|
360
|
-
if (
|
|
361
|
-
this.normalCompaction ||
|
|
362
|
-
this.rollover?.phase === "compacting" ||
|
|
363
|
-
event.reason === "overflow"
|
|
364
|
-
) {
|
|
360
|
+
if (this.allowsCompaction(event.reason)) {
|
|
365
361
|
// PCC owns preparation when bridged, including native checkpoint routes.
|
|
366
362
|
if (this.bridge) return;
|
|
367
363
|
try {
|
|
@@ -435,6 +431,13 @@ export class NotesLifecycle {
|
|
|
435
431
|
};
|
|
436
432
|
return { cancel: true };
|
|
437
433
|
}
|
|
434
|
+
allowsCompaction(reason: SessionBeforeCompactEvent["reason"]): boolean {
|
|
435
|
+
return (
|
|
436
|
+
this.normalCompaction ||
|
|
437
|
+
this.rollover?.phase === "compacting" ||
|
|
438
|
+
reason === "overflow"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
438
441
|
async compactFailed(
|
|
439
442
|
event: SessionCompactFailedEvent,
|
|
440
443
|
ctx: ExtensionContext,
|
|
@@ -450,7 +453,7 @@ export class NotesLifecycle {
|
|
|
450
453
|
)
|
|
451
454
|
return;
|
|
452
455
|
if (!pending.instructions?.trim() && this.fresh(ctx))
|
|
453
|
-
await this.roll(ctx,
|
|
456
|
+
await this.roll(ctx, false);
|
|
454
457
|
else this.startCheckpoint(ctx, pending.instructions);
|
|
455
458
|
}
|
|
456
459
|
async settled(
|
|
@@ -517,7 +520,7 @@ export class NotesLifecycle {
|
|
|
517
520
|
);
|
|
518
521
|
return;
|
|
519
522
|
}
|
|
520
|
-
await this.roll(ctx,
|
|
523
|
+
await this.roll(ctx, false);
|
|
521
524
|
return;
|
|
522
525
|
}
|
|
523
526
|
if (this.held?.phase === "draining") {
|
package/src/notes.ts
CHANGED
|
@@ -216,25 +216,34 @@ export function exportNotesSnapshot(
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
function canonicalPath(path: unknown, agent: string, required = true): string {
|
|
219
|
-
if (
|
|
220
|
-
if (required) throw new Error("
|
|
219
|
+
if (path === undefined || path === null || path === "") {
|
|
220
|
+
if (required) throw new Error("Note file path is required");
|
|
221
221
|
return "";
|
|
222
222
|
}
|
|
223
|
+
if (typeof path !== "string")
|
|
224
|
+
throw new Error(
|
|
225
|
+
required ? "Note file path is required" : "Note prefix must be a string",
|
|
226
|
+
);
|
|
223
227
|
const root = `${agent}/notes`;
|
|
224
228
|
let value = path;
|
|
225
|
-
if (value === root)
|
|
226
|
-
|
|
229
|
+
if (value === root) {
|
|
230
|
+
if (required)
|
|
231
|
+
throw new Error("Absolute note paths must use <agent>/notes/<path>");
|
|
232
|
+
value = "";
|
|
233
|
+
} else if (value.startsWith(`${root}/`)) value = value.slice(root.length + 1);
|
|
227
234
|
else if (value.startsWith("/"))
|
|
228
235
|
throw new Error(
|
|
229
236
|
"That agent's notes are not available here. Use a relative path for this agent.",
|
|
230
237
|
);
|
|
231
238
|
if (
|
|
232
239
|
(required && !value) ||
|
|
233
|
-
value
|
|
234
|
-
|
|
240
|
+
(value !== "" &&
|
|
241
|
+
value.split("/").some((part) => !part || part === ".." || part === "."))
|
|
235
242
|
)
|
|
236
243
|
throw new Error(
|
|
237
|
-
|
|
244
|
+
required
|
|
245
|
+
? "Note paths cannot contain empty, . or .. components"
|
|
246
|
+
: "Note prefixes cannot contain empty, . or .. components",
|
|
238
247
|
);
|
|
239
248
|
return value;
|
|
240
249
|
}
|
|
@@ -244,11 +253,8 @@ export function normalizeNoteParams(
|
|
|
244
253
|
agent: string,
|
|
245
254
|
): Record<string, unknown> {
|
|
246
255
|
const result = { ...params };
|
|
247
|
-
const needsPath = ["read_file", "write_file", "append_to_file"].includes(
|
|
248
|
-
String(params["action"]),
|
|
249
|
-
);
|
|
250
256
|
for (const field of ["path", "prefix", "path_prefix"])
|
|
251
|
-
if (field in params
|
|
257
|
+
if (field in params)
|
|
252
258
|
result[field] = canonicalPath(params[field], agent, field === "path");
|
|
253
259
|
return result;
|
|
254
260
|
}
|
|
@@ -285,12 +291,13 @@ export function useNotes(
|
|
|
285
291
|
if (action === "write_file" || action === "append_to_file") {
|
|
286
292
|
const path = params["path"] as string;
|
|
287
293
|
const text = params["text"];
|
|
288
|
-
if (typeof text !== "string")
|
|
294
|
+
if (typeof text !== "string")
|
|
295
|
+
throw new Error(`notes ${action} requires text`);
|
|
289
296
|
const previous = notes.get(path);
|
|
290
297
|
const next =
|
|
291
298
|
action === "append_to_file" ? (previous?.text ?? "") + text : text;
|
|
292
299
|
if (Buffer.byteLength(next) > 1_000_000)
|
|
293
|
-
throw new Error("Note exceeds 1
|
|
300
|
+
throw new Error("Note file exceeds the 1,000,000-byte limit");
|
|
294
301
|
const now = Date.now();
|
|
295
302
|
// Append never pretends to decode a remote base. The remote fragment remains separate.
|
|
296
303
|
const mode =
|
|
@@ -324,113 +331,101 @@ export function useNotes(
|
|
|
324
331
|
);
|
|
325
332
|
}
|
|
326
333
|
return {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
operation:
|
|
330
|
-
mode === "append" ? "local_append_overlay" : "local_replacement",
|
|
334
|
+
source: "pi-session",
|
|
335
|
+
file: noteMetadata(revision, root),
|
|
331
336
|
};
|
|
332
337
|
}
|
|
338
|
+
|
|
333
339
|
if (action === "read_file") {
|
|
334
340
|
const path = params["path"] as string;
|
|
335
341
|
const note = notes.get(path);
|
|
336
|
-
if (!note) return {
|
|
342
|
+
if (!note) return { source: "pi-session", file: null };
|
|
337
343
|
const lines = note.text.split("\n");
|
|
338
|
-
const start =
|
|
339
|
-
const stop =
|
|
340
|
-
let text = "";
|
|
341
|
-
let next = start;
|
|
342
|
-
while (next <= stop) {
|
|
343
|
-
const line = lines[next - 1] ?? "";
|
|
344
|
-
if (text.length + line.length > 32_000) {
|
|
345
|
-
if (!text)
|
|
346
|
-
throw new Error(
|
|
347
|
-
"This note line exceeds 32000 characters. Rewrite it as shorter lines to read it.",
|
|
348
|
-
);
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
text += `${next > start ? "\n" : ""}${line}`;
|
|
352
|
-
next++;
|
|
353
|
-
}
|
|
344
|
+
const start = lineIndex(params["start_line"], lines.length, 0);
|
|
345
|
+
const stop = lineIndex(params["stop_line"], lines.length, lines.length - 1);
|
|
354
346
|
return {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
347
|
+
source: "pi-session",
|
|
348
|
+
file: {
|
|
349
|
+
...noteMetadata(note, root),
|
|
350
|
+
content: start <= stop ? lines.slice(start, stop + 1).join("\n") : "",
|
|
351
|
+
start_line: start + 1,
|
|
352
|
+
stop_line: Math.max(start, stop) + 1,
|
|
353
|
+
total_lines: lines.length,
|
|
354
|
+
},
|
|
362
355
|
};
|
|
363
356
|
}
|
|
364
357
|
if (action === "list_files_by_prefix") {
|
|
365
|
-
let files = [...notes.values()].filter((note) =>
|
|
366
|
-
note.path.startsWith(String(params["prefix"] ?? "")),
|
|
367
|
-
);
|
|
368
358
|
const by = params["file_order_by"];
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
})),
|
|
384
|
-
truncated: files.length > maximum,
|
|
385
|
-
};
|
|
359
|
+
const direction = params["file_order"] === "descending" ? -1 : 1;
|
|
360
|
+
const files = [...notes.values()]
|
|
361
|
+
.filter((note) => note.path.startsWith(String(params["prefix"] ?? "")))
|
|
362
|
+
.sort(
|
|
363
|
+
(a, b) =>
|
|
364
|
+
(by === "created_at"
|
|
365
|
+
? a.createdAt - b.createdAt
|
|
366
|
+
: by === "updated_at"
|
|
367
|
+
? a.updatedAt - b.updatedAt
|
|
368
|
+
: a.path.localeCompare(b.path)) * direction,
|
|
369
|
+
)
|
|
370
|
+
.slice(0, integer(params["max_results"], 20, 100))
|
|
371
|
+
.map((note) => noteMetadata(note, root));
|
|
372
|
+
return { source: "pi-session", files };
|
|
386
373
|
}
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
throw new Error("Supply a nonempty search query");
|
|
374
|
+
if (action !== "search_contents")
|
|
375
|
+
throw new Error("notes requires a supported action");
|
|
376
|
+
const query = params["query"];
|
|
377
|
+
if (typeof query !== "string" || !query)
|
|
378
|
+
throw new Error("notes search_contents requires query");
|
|
393
379
|
const files = [...notes.values()].filter((note) =>
|
|
394
380
|
note.path.startsWith(String(params["path_prefix"] ?? "")),
|
|
395
381
|
);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}[] = [];
|
|
403
|
-
let chars = 0;
|
|
404
|
-
let truncated = false;
|
|
382
|
+
files.sort(
|
|
383
|
+
params["recent_file_first"] === true
|
|
384
|
+
? (a, b) => b.createdAt - a.createdAt
|
|
385
|
+
: (a, b) => a.path.localeCompare(b.path),
|
|
386
|
+
);
|
|
387
|
+
const found: Record<string, unknown>[] = [];
|
|
405
388
|
for (const note of files) {
|
|
406
|
-
const
|
|
389
|
+
const matches = note.text
|
|
407
390
|
.split("\n")
|
|
408
|
-
.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
);
|
|
413
|
-
if (!all.length) continue;
|
|
414
|
-
const matches = all.slice(
|
|
415
|
-
0,
|
|
416
|
-
integer(params["max_matches_per_file"], 10, 100),
|
|
417
|
-
);
|
|
418
|
-
const size = JSON.stringify(matches).length;
|
|
419
|
-
if (
|
|
420
|
-
found.length >= integer(params["max_files"], 20, 100) ||
|
|
421
|
-
chars + size > 32_000
|
|
422
|
-
) {
|
|
423
|
-
truncated = true;
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
391
|
+
.map((line, index) => ({ line_number: index + 1, line }))
|
|
392
|
+
.filter(({ line }) => line.includes(query))
|
|
393
|
+
.slice(0, integer(params["max_matches_per_file"], 20, 100));
|
|
394
|
+
if (!matches.length) continue;
|
|
426
395
|
found.push({
|
|
427
396
|
path: `${root}${note.path}`,
|
|
428
397
|
matches,
|
|
429
|
-
|
|
398
|
+
operation:
|
|
399
|
+
note.mode === "replace" ? "local_replacement" : "local_append_overlay",
|
|
430
400
|
});
|
|
431
|
-
|
|
401
|
+
if (found.length >= integer(params["max_files"], 20, 100)) break;
|
|
432
402
|
}
|
|
433
|
-
return { files: found
|
|
403
|
+
return { source: "pi-session", files: found };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function noteMetadata(
|
|
407
|
+
note: NoteRevision,
|
|
408
|
+
root: string,
|
|
409
|
+
): Record<string, unknown> {
|
|
410
|
+
return {
|
|
411
|
+
operation:
|
|
412
|
+
note.mode === "replace" ? "local_replacement" : "local_append_overlay",
|
|
413
|
+
path: `${root}${note.path}`,
|
|
414
|
+
bytes: Buffer.byteLength(note.text, "utf8"),
|
|
415
|
+
created_at: new Date(note.createdAt).toISOString(),
|
|
416
|
+
updated_at: new Date(note.updatedAt).toISOString(),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function lineIndex(
|
|
421
|
+
value: unknown,
|
|
422
|
+
lineCount: number,
|
|
423
|
+
fallback: number,
|
|
424
|
+
): number {
|
|
425
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value === 0)
|
|
426
|
+
return fallback;
|
|
427
|
+
const index = value > 0 ? value - 1 : lineCount + value;
|
|
428
|
+
return Math.max(0, Math.min(index, Math.max(0, lineCount - 1)));
|
|
434
429
|
}
|
|
435
430
|
|
|
436
431
|
function integer(value: unknown, fallback: number, max: number): number {
|
package/src/settings-screen.ts
CHANGED
|
@@ -27,7 +27,7 @@ export async function openNotesSettings(
|
|
|
27
27
|
id: "compact",
|
|
28
28
|
label: "Normal compaction",
|
|
29
29
|
description:
|
|
30
|
-
"
|
|
30
|
+
"Use Pi/PCC compaction for /compact and new_context. Off uses notes-only rollover; /compact then waits for your next prompt.",
|
|
31
31
|
currentValue: settings.normalCompaction ? "on" : "off",
|
|
32
32
|
values: ["off", "on"],
|
|
33
33
|
},
|
package/src/tools.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ToolDefinition,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Type } from "typebox";
|
|
9
|
+
import { Check } from "typebox/value";
|
|
9
10
|
import type {
|
|
10
11
|
CachedResponse,
|
|
11
12
|
LookupQuery,
|
|
@@ -44,16 +45,18 @@ export const NOTES_USAGE = "await tools.notes() // help";
|
|
|
44
45
|
export const HISTORY_USAGE = "await tools.history() // help";
|
|
45
46
|
const STRING = Type.Optional(Type.String());
|
|
46
47
|
const INTEGER = Type.Optional(Type.Integer({ minimum: 1 }));
|
|
48
|
+
const NULLABLE_STRING = Type.Optional(Type.Union([Type.String(), Type.Null()]));
|
|
49
|
+
const LINE = Type.Optional(Type.Union([Type.Integer(), Type.Null()]));
|
|
47
50
|
const NOTES_SCHEMA = Type.Object(
|
|
48
51
|
{
|
|
49
52
|
action: Type.Optional(StringEnum(NOTES_ACTIONS)),
|
|
50
53
|
path: STRING,
|
|
51
54
|
text: STRING,
|
|
52
|
-
prefix:
|
|
55
|
+
prefix: NULLABLE_STRING,
|
|
53
56
|
query: STRING,
|
|
54
|
-
path_prefix:
|
|
55
|
-
start_line:
|
|
56
|
-
stop_line:
|
|
57
|
+
path_prefix: NULLABLE_STRING,
|
|
58
|
+
start_line: LINE,
|
|
59
|
+
stop_line: LINE,
|
|
57
60
|
max_results: INTEGER,
|
|
58
61
|
max_files: INTEGER,
|
|
59
62
|
max_matches_per_file: INTEGER,
|
|
@@ -146,7 +149,32 @@ function relevant(
|
|
|
146
149
|
const key =
|
|
147
150
|
action === "search_contents" ? `search_${namespace}` : String(action);
|
|
148
151
|
const fields = FIELDS[key];
|
|
149
|
-
if (!fields)
|
|
152
|
+
if (!fields)
|
|
153
|
+
throw new Error(
|
|
154
|
+
namespace === "notes"
|
|
155
|
+
? "notes requires a supported action"
|
|
156
|
+
: "Unknown action",
|
|
157
|
+
);
|
|
158
|
+
if (namespace === "notes") {
|
|
159
|
+
for (const field of Object.keys(params))
|
|
160
|
+
if (field !== "action" && !fields.includes(field))
|
|
161
|
+
throw new Error(`notes ${String(action)} does not accept ${field}`);
|
|
162
|
+
if (
|
|
163
|
+
["read_file", "write_file", "append_to_file"].includes(String(action)) &&
|
|
164
|
+
(typeof params["path"] !== "string" || !params["path"])
|
|
165
|
+
)
|
|
166
|
+
throw new Error(`notes ${String(action)} requires path`);
|
|
167
|
+
if (
|
|
168
|
+
action === "search_contents" &&
|
|
169
|
+
(typeof params["query"] !== "string" || !params["query"])
|
|
170
|
+
)
|
|
171
|
+
throw new Error("notes search_contents requires query");
|
|
172
|
+
if (
|
|
173
|
+
["write_file", "append_to_file"].includes(String(action)) &&
|
|
174
|
+
typeof params["text"] !== "string"
|
|
175
|
+
)
|
|
176
|
+
throw new Error(`notes ${String(action)} requires text`);
|
|
177
|
+
}
|
|
150
178
|
return Object.fromEntries(
|
|
151
179
|
Object.entries(params).filter(
|
|
152
180
|
([key]) => key === "action" || fields.includes(key),
|
|
@@ -192,6 +220,16 @@ function discovery(
|
|
|
192
220
|
).join(" "),
|
|
193
221
|
]),
|
|
194
222
|
),
|
|
223
|
+
...(namespace === "notes"
|
|
224
|
+
? {
|
|
225
|
+
usage:
|
|
226
|
+
'await tools.notes({ action: "read_file", path: "checkpoint" })',
|
|
227
|
+
ranges:
|
|
228
|
+
"start_line/stop_line inclusive, 1-based; negative counts from end; 0/null/omitted uses first/last line; out-of-range clamps; reversed range returns empty content",
|
|
229
|
+
defaults:
|
|
230
|
+
"list max_results 20; search max_files/max_matches_per_file 20; limits clamp to 1..100; list name ascending; search name ascending, recent_file_first sorts created_at descending",
|
|
231
|
+
}
|
|
232
|
+
: {}),
|
|
195
233
|
});
|
|
196
234
|
}
|
|
197
235
|
return undefined;
|
|
@@ -484,15 +522,29 @@ export function createTools(
|
|
|
484
522
|
prepareArguments(args) {
|
|
485
523
|
if (args === undefined) return {};
|
|
486
524
|
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
487
|
-
|
|
525
|
+
throw new Error(
|
|
526
|
+
'Supply an action object: await tools.notes({ action: "read_file", path: "checkpoint" }); tools.notes() returns help',
|
|
527
|
+
);
|
|
488
528
|
const params = { ...args } as Record<string, unknown>;
|
|
489
529
|
if (
|
|
490
530
|
params["action"] === "list_files_by_prefix" &&
|
|
491
531
|
params["max_files"] !== undefined
|
|
492
532
|
) {
|
|
493
|
-
|
|
533
|
+
if (!("max_results" in params))
|
|
534
|
+
params["max_results"] = params["max_files"];
|
|
494
535
|
delete params["max_files"];
|
|
495
536
|
}
|
|
537
|
+
if (!discovery("notes", params)) relevant("notes", params);
|
|
538
|
+
for (const [field, value] of Object.entries(params)) {
|
|
539
|
+
const schema =
|
|
540
|
+
NOTES_SCHEMA.properties[
|
|
541
|
+
field as keyof typeof NOTES_SCHEMA.properties
|
|
542
|
+
];
|
|
543
|
+
if (!schema || !Check(schema, value))
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Invalid notes ${field}; call tools.notes() for exact arguments`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
496
548
|
return params;
|
|
497
549
|
},
|
|
498
550
|
execute: (callId, input, signal, _update, ctx) =>
|