@howaboua/pi-notes-compaction 0.0.0-stage → 0.0.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 +5 -0
- package/PCC-INTEGRATION.md +100 -0
- package/README.md +42 -2
- package/changelog.ts +337 -0
- package/package.json +50 -5
- package/src/bridge.ts +156 -0
- package/src/fresh-notes.ts +132 -0
- package/src/history.ts +198 -0
- package/src/index.ts +274 -0
- package/src/lifecycle.ts +538 -0
- package/src/notes.ts +440 -0
- package/src/settings.ts +48 -0
- package/src/store.ts +240 -0
- package/src/tools.ts +554 -0
- package/src/windows.ts +282 -0
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type {
|
|
3
|
+
AgentToolResult,
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
SessionEntry,
|
|
7
|
+
ToolDefinition,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
export const BRIDGE_CHANNEL = "pi-notes-compaction:bridge:v1";
|
|
11
|
+
export const OWNER_CHANNEL = "pi-notes-compaction:owner:v1";
|
|
12
|
+
export const TOOL_NAMES = [
|
|
13
|
+
"notes",
|
|
14
|
+
"history",
|
|
15
|
+
"new_context",
|
|
16
|
+
"get_context_remaining",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export interface LookupQuery {
|
|
20
|
+
namespace: "notes" | "history";
|
|
21
|
+
params: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Raw backend response, not a reconstructed ciphertext or an old delivery receipt. */
|
|
25
|
+
export interface CachedResponse {
|
|
26
|
+
query: LookupQuery;
|
|
27
|
+
bytes: Uint8Array;
|
|
28
|
+
source: string;
|
|
29
|
+
encoding: string;
|
|
30
|
+
fetchedAt: number;
|
|
31
|
+
coverage: "complete" | "partial" | "unknown";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WindowIdentity {
|
|
35
|
+
firstWindowId: string;
|
|
36
|
+
currentWindowId: string;
|
|
37
|
+
previousWindowId?: string;
|
|
38
|
+
windowNumber: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface NoteSnapshotData {
|
|
42
|
+
protocol: 1;
|
|
43
|
+
timestamp: number;
|
|
44
|
+
files: Array<{
|
|
45
|
+
path: string;
|
|
46
|
+
text: string;
|
|
47
|
+
createdAt: number;
|
|
48
|
+
updatedAt: number;
|
|
49
|
+
operation: "local_append_overlay" | "local_replacement";
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** JSON-safe raw provenance. Projection belongs to the requesting runtime. */
|
|
54
|
+
export interface SerializedCachedResponse
|
|
55
|
+
extends Omit<CachedResponse, "bytes"> {
|
|
56
|
+
bytesBase64: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SharedNotesDetails {
|
|
60
|
+
codexHistoryNotes: Record<string, unknown>;
|
|
61
|
+
externalNotesResponses?: SerializedCachedResponse[];
|
|
62
|
+
notesCompactionRouted?: true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface NotesOwner {
|
|
66
|
+
protocol: 1;
|
|
67
|
+
owner: "pi-notes-compaction";
|
|
68
|
+
identity(ctx: ExtensionContext): WindowIdentity | undefined;
|
|
69
|
+
projectMessages(messages: readonly AgentMessage[]): AgentMessage[];
|
|
70
|
+
projectBranch(entries: readonly SessionEntry[]): SessionEntry[];
|
|
71
|
+
canBind?(ctx: ExtensionContext): boolean;
|
|
72
|
+
snapshotNotes?(
|
|
73
|
+
entries: readonly SessionEntry[],
|
|
74
|
+
agentName: string,
|
|
75
|
+
): NoteSnapshotData;
|
|
76
|
+
executeShared?(
|
|
77
|
+
query: LookupQuery,
|
|
78
|
+
callId: string,
|
|
79
|
+
ctx: ExtensionContext,
|
|
80
|
+
signal?: AbortSignal,
|
|
81
|
+
): Promise<AgentToolResult<SharedNotesDetails>>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface NotesBridge {
|
|
85
|
+
protocol: 1;
|
|
86
|
+
/** Synchronous atomic handoff. No legacy registrations or lifecycle owners remain active. */
|
|
87
|
+
claim(owner: NotesOwner):
|
|
88
|
+
| {
|
|
89
|
+
claimed: true;
|
|
90
|
+
registerTools(
|
|
91
|
+
tools: readonly ToolDefinition[],
|
|
92
|
+
ctx: ExtensionContext,
|
|
93
|
+
): void;
|
|
94
|
+
}
|
|
95
|
+
| { claimed: false; reason: string };
|
|
96
|
+
/** Configures PCC nested registrations, visibility and opaque-result capture. Never nests new_context. */
|
|
97
|
+
configureTools?(
|
|
98
|
+
tools: readonly ToolDefinition[],
|
|
99
|
+
ctx: ExtensionContext,
|
|
100
|
+
contracts: Readonly<Record<"notes" | "history", string>>,
|
|
101
|
+
): void;
|
|
102
|
+
beforeWindow?(ctx: ExtensionContext, signal?: AbortSignal): Promise<void>;
|
|
103
|
+
lookup?(
|
|
104
|
+
query: LookupQuery,
|
|
105
|
+
ctx: ExtensionContext,
|
|
106
|
+
signal?: AbortSignal,
|
|
107
|
+
): Promise<readonly CachedResponse[]>;
|
|
108
|
+
canProject?(ctx: ExtensionContext): boolean;
|
|
109
|
+
/** Fresh current-call delivery, including original exec ancestry when nested. */
|
|
110
|
+
projectResult?(
|
|
111
|
+
result: AgentToolResult<unknown>,
|
|
112
|
+
responses: readonly CachedResponse[],
|
|
113
|
+
callId: string,
|
|
114
|
+
ctx: ExtensionContext,
|
|
115
|
+
signal?: AbortSignal,
|
|
116
|
+
): Promise<AgentToolResult<unknown>>;
|
|
117
|
+
agentName?(ctx: ExtensionContext): string;
|
|
118
|
+
/** Optional family routing. undefined means this agent owns the requested path. */
|
|
119
|
+
route?(
|
|
120
|
+
query: LookupQuery,
|
|
121
|
+
callId: string,
|
|
122
|
+
ctx: ExtensionContext,
|
|
123
|
+
signal?: AbortSignal,
|
|
124
|
+
): Promise<AgentToolResult<unknown> | undefined>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface BridgeRequest {
|
|
128
|
+
protocol: 1;
|
|
129
|
+
reply(bridge: NotesBridge): void;
|
|
130
|
+
}
|
|
131
|
+
export interface OwnerRequest {
|
|
132
|
+
protocol: 1;
|
|
133
|
+
reply(owner: NotesOwner): void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function discoverBridge(pi: ExtensionAPI): NotesBridge | undefined {
|
|
137
|
+
let found: NotesBridge | undefined;
|
|
138
|
+
pi.events.emit(BRIDGE_CHANNEL, {
|
|
139
|
+
protocol: 1,
|
|
140
|
+
reply(bridge: NotesBridge) {
|
|
141
|
+
if (bridge?.protocol !== 1 || typeof bridge.claim !== "function")
|
|
142
|
+
throw new Error("Unsupported notes bridge");
|
|
143
|
+
if (found && found !== bridge)
|
|
144
|
+
throw new Error("Multiple notes bridges responded");
|
|
145
|
+
found = bridge;
|
|
146
|
+
},
|
|
147
|
+
} satisfies BridgeRequest);
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function queryKey(query: LookupQuery): string {
|
|
152
|
+
return JSON.stringify([
|
|
153
|
+
query.namespace,
|
|
154
|
+
Object.entries(query.params).sort(([a], [b]) => a.localeCompare(b)),
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSessionProjection,
|
|
3
|
+
type SessionEntry,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { NOTE_RECEIPT, noteReceipt } from "./notes.js";
|
|
6
|
+
|
|
7
|
+
/** A receipt proves persistence. Current-run tool outcomes prove the checkpoint actually completed. */
|
|
8
|
+
export function hasFreshNotes(
|
|
9
|
+
branch: readonly SessionEntry[],
|
|
10
|
+
windowId: string | undefined,
|
|
11
|
+
runId?: string,
|
|
12
|
+
): boolean {
|
|
13
|
+
if (!windowId) return false;
|
|
14
|
+
const lastUser = branch.findLastIndex(
|
|
15
|
+
(entry) => entry.type === "message" && entry.message.role === "user",
|
|
16
|
+
);
|
|
17
|
+
const run = branch.slice(lastUser + 1);
|
|
18
|
+
const receipts = run
|
|
19
|
+
.filter(
|
|
20
|
+
(entry) => entry.type === "custom" && entry.customType === NOTE_RECEIPT,
|
|
21
|
+
)
|
|
22
|
+
.flatMap((entry) =>
|
|
23
|
+
entry.type === "custom" ? [noteReceipt(entry.data)] : [],
|
|
24
|
+
)
|
|
25
|
+
.filter(
|
|
26
|
+
(receipt) =>
|
|
27
|
+
receipt &&
|
|
28
|
+
receipt.origin !== "routed" &&
|
|
29
|
+
receipt.windowId === windowId &&
|
|
30
|
+
(!runId || receipt.runId === runId),
|
|
31
|
+
);
|
|
32
|
+
if (!receipts.length) return false;
|
|
33
|
+
const messages = buildSessionProjection(run).messages;
|
|
34
|
+
const final = messages.findLast((message) => message.role === "assistant");
|
|
35
|
+
if (
|
|
36
|
+
final?.role !== "assistant" ||
|
|
37
|
+
final.stopReason !== "stop" ||
|
|
38
|
+
final.content.some((block) => block.type === "toolCall")
|
|
39
|
+
)
|
|
40
|
+
return false;
|
|
41
|
+
const calls = messages.flatMap((message) =>
|
|
42
|
+
message.role === "assistant"
|
|
43
|
+
? message.content.filter((block) => block.type === "toolCall")
|
|
44
|
+
: [],
|
|
45
|
+
);
|
|
46
|
+
const results = messages.flatMap((message) =>
|
|
47
|
+
message.role === "toolResult" ? [message] : [],
|
|
48
|
+
);
|
|
49
|
+
const writes = calls.filter(
|
|
50
|
+
(call) =>
|
|
51
|
+
call.name === "notes" &&
|
|
52
|
+
(call.arguments["action"] === "write_file" ||
|
|
53
|
+
call.arguments["action"] === "append_to_file"),
|
|
54
|
+
);
|
|
55
|
+
let completedWrite = false;
|
|
56
|
+
for (const call of writes) {
|
|
57
|
+
const result = results.find(
|
|
58
|
+
(result) =>
|
|
59
|
+
result.toolCallId === call.id && result.toolName === call.name,
|
|
60
|
+
);
|
|
61
|
+
if (
|
|
62
|
+
result?.details &&
|
|
63
|
+
typeof result.details === "object" &&
|
|
64
|
+
"notesCompactionRouted" in result.details &&
|
|
65
|
+
result.details["notesCompactionRouted"] === true
|
|
66
|
+
)
|
|
67
|
+
continue;
|
|
68
|
+
if (
|
|
69
|
+
!result ||
|
|
70
|
+
result.isError ||
|
|
71
|
+
!result.details ||
|
|
72
|
+
typeof result.details !== "object" ||
|
|
73
|
+
!("notesCompaction" in result.details)
|
|
74
|
+
)
|
|
75
|
+
return false;
|
|
76
|
+
const details = result.details["notesCompaction"];
|
|
77
|
+
if (
|
|
78
|
+
!details ||
|
|
79
|
+
typeof details !== "object" ||
|
|
80
|
+
!("protocol" in details) ||
|
|
81
|
+
details["protocol"] !== 1 ||
|
|
82
|
+
!("saved" in details) ||
|
|
83
|
+
details["saved"] !== true ||
|
|
84
|
+
!("runId" in details) ||
|
|
85
|
+
!receipts.some((receipt) => receipt?.runId === details["runId"])
|
|
86
|
+
)
|
|
87
|
+
return false;
|
|
88
|
+
completedWrite = true;
|
|
89
|
+
}
|
|
90
|
+
for (const result of results) {
|
|
91
|
+
const details = result.details;
|
|
92
|
+
const saved =
|
|
93
|
+
details && typeof details === "object" && "contextNotesSaved" in details
|
|
94
|
+
? details["contextNotesSaved"]
|
|
95
|
+
: undefined;
|
|
96
|
+
// PCC's protected exec/wait host records cover nested calls outside Pi's executeTool path.
|
|
97
|
+
if (saved === false || (saved === true && result.isError)) return false;
|
|
98
|
+
if (
|
|
99
|
+
saved === true &&
|
|
100
|
+
(result.toolName === "exec" || result.toolName === "wait")
|
|
101
|
+
)
|
|
102
|
+
completedWrite = true;
|
|
103
|
+
const nested = result.nestedCalls;
|
|
104
|
+
const noteCalls =
|
|
105
|
+
nested?.calls.filter((call) => call.name === "notes") ?? [];
|
|
106
|
+
if (
|
|
107
|
+
noteCalls.some(
|
|
108
|
+
(call) =>
|
|
109
|
+
call.status !== "ok" &&
|
|
110
|
+
(!call.arguments ||
|
|
111
|
+
call.arguments["action"] === "write_file" ||
|
|
112
|
+
call.arguments["action"] === "append_to_file"),
|
|
113
|
+
) ||
|
|
114
|
+
(noteCalls.length && !nested?.complete)
|
|
115
|
+
)
|
|
116
|
+
return false;
|
|
117
|
+
const nestedWrites =
|
|
118
|
+
nested?.calls.filter(
|
|
119
|
+
(call) =>
|
|
120
|
+
call.name === "notes" &&
|
|
121
|
+
(call.arguments?.["action"] === "write_file" ||
|
|
122
|
+
call.arguments?.["action"] === "append_to_file"),
|
|
123
|
+
) ?? [];
|
|
124
|
+
if (
|
|
125
|
+
nestedWrites.some((call) => call.status !== "ok") ||
|
|
126
|
+
(nestedWrites.length && (result.isError || !nested?.complete))
|
|
127
|
+
)
|
|
128
|
+
return false;
|
|
129
|
+
if (nestedWrites.length) completedWrite = true;
|
|
130
|
+
}
|
|
131
|
+
return completedWrite;
|
|
132
|
+
}
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { windowDetails } from "./windows.js";
|
|
3
|
+
|
|
4
|
+
interface HistoryItem {
|
|
5
|
+
window_id: string;
|
|
6
|
+
item_id: string;
|
|
7
|
+
role: string;
|
|
8
|
+
tool_name?: string;
|
|
9
|
+
tool_namespace?: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function customHistoryRole(
|
|
14
|
+
entry: Extract<SessionEntry, { type: "custom_message" }>,
|
|
15
|
+
): string {
|
|
16
|
+
// Persisted developer metadata is a wire contract, not a package dependency.
|
|
17
|
+
const details = entry.details;
|
|
18
|
+
const metadata =
|
|
19
|
+
details &&
|
|
20
|
+
typeof details === "object" &&
|
|
21
|
+
"@howaboua/pi-codex-conversion/developer-message" in details
|
|
22
|
+
? details["@howaboua/pi-codex-conversion/developer-message"]
|
|
23
|
+
: [
|
|
24
|
+
"codex-developer-message",
|
|
25
|
+
"codex-context-window",
|
|
26
|
+
"codex-current-time-reminder",
|
|
27
|
+
"codex-toolkit-update",
|
|
28
|
+
"codex-notebook-status",
|
|
29
|
+
"codex-context-briefing",
|
|
30
|
+
].includes(entry.customType)
|
|
31
|
+
? details
|
|
32
|
+
: undefined;
|
|
33
|
+
return metadata &&
|
|
34
|
+
typeof metadata === "object" &&
|
|
35
|
+
"protocol" in metadata &&
|
|
36
|
+
metadata.protocol === 1 &&
|
|
37
|
+
"id" in metadata &&
|
|
38
|
+
typeof metadata.id === "string" &&
|
|
39
|
+
metadata.id.trim() !== ""
|
|
40
|
+
? "developer"
|
|
41
|
+
: "user";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Pi's selected JSONL branch is the archive. No transcript database or Tree backend. */
|
|
45
|
+
export function useHistory(
|
|
46
|
+
entries: readonly SessionEntry[],
|
|
47
|
+
params: Record<string, unknown>,
|
|
48
|
+
sessionId: string,
|
|
49
|
+
): Record<string, unknown> {
|
|
50
|
+
let windowId = `session:${sessionId}`;
|
|
51
|
+
const windows = new Map<string, HistoryItem[]>();
|
|
52
|
+
windows.set(windowId, []);
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const boundary = windowDetails(entry);
|
|
55
|
+
if (boundary?.kind === "window") {
|
|
56
|
+
windowId = boundary.identity.currentWindowId;
|
|
57
|
+
if (!windows.has(windowId)) windows.set(windowId, []);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (entry.type === "message") {
|
|
61
|
+
const message = entry.message;
|
|
62
|
+
const call =
|
|
63
|
+
message.role === "assistant"
|
|
64
|
+
? message.content.find((block) => block.type === "toolCall")
|
|
65
|
+
: undefined;
|
|
66
|
+
const toolName =
|
|
67
|
+
message.role === "toolResult"
|
|
68
|
+
? message.toolName
|
|
69
|
+
: call?.type === "toolCall"
|
|
70
|
+
? call.name
|
|
71
|
+
: undefined;
|
|
72
|
+
const content = !("content" in message)
|
|
73
|
+
? JSON.stringify(message)
|
|
74
|
+
: typeof message.content === "string"
|
|
75
|
+
? message.content
|
|
76
|
+
: message.content
|
|
77
|
+
.map((block) => {
|
|
78
|
+
if (block.type === "text") return block.text;
|
|
79
|
+
if (block.type === "thinking") return block.thinking;
|
|
80
|
+
if (block.type === "toolCall")
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
tool: block.name,
|
|
83
|
+
arguments: block.arguments,
|
|
84
|
+
});
|
|
85
|
+
if (block.type === "image") return `[image ${block.mimeType}]`;
|
|
86
|
+
return JSON.stringify(block);
|
|
87
|
+
})
|
|
88
|
+
.join("\n");
|
|
89
|
+
windows.get(windowId)!.push({
|
|
90
|
+
window_id: windowId,
|
|
91
|
+
item_id: entry.id,
|
|
92
|
+
role: message.role === "toolResult" ? "tool" : message.role,
|
|
93
|
+
...(toolName
|
|
94
|
+
? {
|
|
95
|
+
tool_name: toolName,
|
|
96
|
+
...(["notes", "history"].includes(toolName)
|
|
97
|
+
? { tool_namespace: toolName }
|
|
98
|
+
: {}),
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
content,
|
|
102
|
+
});
|
|
103
|
+
} else if (entry.type === "custom_message" && !boundary) {
|
|
104
|
+
windows.get(windowId)!.push({
|
|
105
|
+
window_id: windowId,
|
|
106
|
+
item_id: entry.id,
|
|
107
|
+
role: customHistoryRole(entry),
|
|
108
|
+
content:
|
|
109
|
+
typeof entry.content === "string"
|
|
110
|
+
? entry.content
|
|
111
|
+
: entry.content
|
|
112
|
+
.map((block) =>
|
|
113
|
+
block.type === "text" ? block.text : "[image]",
|
|
114
|
+
)
|
|
115
|
+
.join("\n"),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const action = params["action"];
|
|
120
|
+
if (action === "list_windows") {
|
|
121
|
+
let values = [...windows].filter(([, items]) => items.length);
|
|
122
|
+
if (params["recent_first"] === true) values = values.reverse();
|
|
123
|
+
const limit = integer(params["limit"], 20, 100);
|
|
124
|
+
return {
|
|
125
|
+
windows: values
|
|
126
|
+
.slice(0, limit)
|
|
127
|
+
.map(([id, items]) => ({ window_id: id, item_count: items.length })),
|
|
128
|
+
truncated: values.length > limit,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (action === "read_item") {
|
|
132
|
+
if (
|
|
133
|
+
typeof params["window_id"] !== "string" ||
|
|
134
|
+
typeof params["item_id"] !== "string"
|
|
135
|
+
)
|
|
136
|
+
throw new Error("Supply window_id and item_id unchanged from history");
|
|
137
|
+
const item = windows
|
|
138
|
+
.get(params["window_id"])
|
|
139
|
+
?.find((item) => item.item_id === params["item_id"]);
|
|
140
|
+
if (!item) return { item: null };
|
|
141
|
+
const offset = integer(params["offset_chars"], 0, item.content.length);
|
|
142
|
+
const limit = integer(params["limit_chars"], 8000, 8000);
|
|
143
|
+
const content = item.content.slice(offset, offset + limit);
|
|
144
|
+
return {
|
|
145
|
+
item: {
|
|
146
|
+
...item,
|
|
147
|
+
content,
|
|
148
|
+
total_chars: item.content.length,
|
|
149
|
+
...(offset + content.length < item.content.length
|
|
150
|
+
? { next_offset_chars: offset + content.length }
|
|
151
|
+
: {}),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (action !== "list_items" && action !== "search_contents")
|
|
156
|
+
throw new Error("Unknown history action");
|
|
157
|
+
if (
|
|
158
|
+
action === "search_contents" &&
|
|
159
|
+
(typeof params["query"] !== "string" || !params["query"])
|
|
160
|
+
)
|
|
161
|
+
throw new Error("Supply a nonempty search query");
|
|
162
|
+
let items = [...windows.values()]
|
|
163
|
+
.flat()
|
|
164
|
+
.filter(
|
|
165
|
+
(item) =>
|
|
166
|
+
(!params["window_id"] || item.window_id === params["window_id"]) &&
|
|
167
|
+
(!params["role"] || item.role === params["role"]) &&
|
|
168
|
+
(!params["tool_name"] || item.tool_name === params["tool_name"]) &&
|
|
169
|
+
(!params["tool_namespace"] ||
|
|
170
|
+
item.tool_namespace === params["tool_namespace"]) &&
|
|
171
|
+
(action !== "search_contents" ||
|
|
172
|
+
item.content.includes(params["query"] as string)),
|
|
173
|
+
);
|
|
174
|
+
if (params["recent_first"] === true) items = items.reverse();
|
|
175
|
+
const limit = integer(params["limit"], 10, 25);
|
|
176
|
+
const previewChars = integer(params["max_chars_per_item"], 1000, 1000);
|
|
177
|
+
const previews = [];
|
|
178
|
+
let size = 0;
|
|
179
|
+
for (const item of items) {
|
|
180
|
+
const { content, ...metadata } = item;
|
|
181
|
+
const preview = {
|
|
182
|
+
...metadata,
|
|
183
|
+
truncated_content: content.slice(0, previewChars),
|
|
184
|
+
content_chars: content.length,
|
|
185
|
+
};
|
|
186
|
+
const chars = JSON.stringify(preview).length;
|
|
187
|
+
if (previews.length >= limit || size + chars > 8000) break;
|
|
188
|
+
previews.push(preview);
|
|
189
|
+
size += chars;
|
|
190
|
+
}
|
|
191
|
+
return { items: previews, truncated: previews.length < items.length };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function integer(value: unknown, fallback: number, max: number): number {
|
|
195
|
+
return typeof value === "number" && Number.isInteger(value)
|
|
196
|
+
? Math.max(0, Math.min(value, max))
|
|
197
|
+
: fallback;
|
|
198
|
+
}
|