opencode-engram 0.1.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/LICENSE +21 -0
- package/README.md +259 -0
- package/README.zh.md +257 -0
- package/README.zht.md +255 -0
- package/package.json +48 -0
- package/src/common/charting.ts +107 -0
- package/src/common/common.ts +74 -0
- package/src/common/config.ts +963 -0
- package/src/common/history-prompt.ts +72 -0
- package/src/common/plugin.ts +593 -0
- package/src/common/upstream-navigator-prompt.ts +131 -0
- package/src/core/index.ts +33 -0
- package/src/core/sdk-bridge.ts +73 -0
- package/src/core/session.ts +196 -0
- package/src/core/turn-index.ts +219 -0
- package/src/domain/adapter.ts +386 -0
- package/src/domain/clip-text.ts +86 -0
- package/src/domain/domain.ts +618 -0
- package/src/domain/preview.ts +132 -0
- package/src/domain/serialize.ts +409 -0
- package/src/domain/types.ts +321 -0
- package/src/runtime/charting.ts +73 -0
- package/src/runtime/debug.ts +155 -0
- package/src/runtime/logger.ts +34 -0
- package/src/runtime/message-io.ts +224 -0
- package/src/runtime/runtime.ts +1033 -0
- package/src/runtime/search.ts +1280 -0
- package/src/runtime/turn-resolve.ts +111 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const builtInHistoryPromptBody = `## Session History
|
|
2
|
+
|
|
3
|
+
Conversation history is the only authoritative source of past state: what was
|
|
4
|
+
done, why, what was decided, and what remains. Treat it as a first-class source
|
|
5
|
+
alongside the local workspace.
|
|
6
|
+
|
|
7
|
+
Retrieve from history when the consequence of NOT having the information is:
|
|
8
|
+
- Wrong direction: acting on a requirement, constraint, or decision you can't
|
|
9
|
+
verify from current context
|
|
10
|
+
- Wrong context: user corrections, rejections, or hard constraints that shaped
|
|
11
|
+
prior work
|
|
12
|
+
- Missing substance: plans, specs, schemas, or analyses not present in current
|
|
13
|
+
context
|
|
14
|
+
|
|
15
|
+
### Efficiency
|
|
16
|
+
|
|
17
|
+
- Prefer search over manual browse-window scanning when you do not already know
|
|
18
|
+
the approximate location of the answer.
|
|
19
|
+
- Do not call pull unless the preview or search snippet is clearly insufficient.
|
|
20
|
+
- Parallel independent calls — multiple searches or pulls that don't depend on
|
|
21
|
+
each other should run in the same round.
|
|
22
|
+
|
|
23
|
+
### Turn Mechanics
|
|
24
|
+
|
|
25
|
+
- history_browse_turns returns visible turns in ascending order and can focus on
|
|
26
|
+
a window around a specific \`turn_index\`.
|
|
27
|
+
- If a requested turn is hidden by self-session filtering, retry with a nearby
|
|
28
|
+
visible turn.
|
|
29
|
+
- history_browse_messages returns a message window around a specific
|
|
30
|
+
\`message_id\`, with \`before_message_id\` and \`after_message_id\` for
|
|
31
|
+
extending the window.
|
|
32
|
+
- Use history_pull_message for full message content.`;
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// State Management
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
export type HistoryPromptState = {
|
|
39
|
+
injectedSessionIds: Set<string>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create shared state for tracking which sessions have received the common
|
|
44
|
+
* history prompt injection.
|
|
45
|
+
*/
|
|
46
|
+
export function createHistoryPromptState(): HistoryPromptState {
|
|
47
|
+
return {
|
|
48
|
+
injectedSessionIds: new Set<string>(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Inject the common history prompt into a session's system prompts, at most
|
|
54
|
+
* once per session.
|
|
55
|
+
*/
|
|
56
|
+
export function injectHistoryPrompt(
|
|
57
|
+
state: HistoryPromptState,
|
|
58
|
+
sessionID: string | undefined,
|
|
59
|
+
system: string[],
|
|
60
|
+
promptBody = builtInHistoryPromptBody,
|
|
61
|
+
): void {
|
|
62
|
+
if (!sessionID) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (state.injectedSessionIds.has(sessionID)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
state.injectedSessionIds.add(sessionID);
|
|
71
|
+
system.push(promptBody);
|
|
72
|
+
}
|
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import {
|
|
2
|
+
tool,
|
|
3
|
+
type Plugin,
|
|
4
|
+
} from "@opencode-ai/plugin";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
invalid,
|
|
8
|
+
type PluginInput,
|
|
9
|
+
} from "./common.ts";
|
|
10
|
+
import {
|
|
11
|
+
loadEngramConfig,
|
|
12
|
+
} from "./config.ts";
|
|
13
|
+
import {
|
|
14
|
+
createUpstreamNavigatorState,
|
|
15
|
+
injectUpstreamNavigatorPrompt,
|
|
16
|
+
recordUpstreamNavigatorSession,
|
|
17
|
+
} from "./upstream-navigator-prompt.ts";
|
|
18
|
+
import {
|
|
19
|
+
createHistoryPromptState,
|
|
20
|
+
injectHistoryPrompt,
|
|
21
|
+
} from "./history-prompt.ts";
|
|
22
|
+
import {
|
|
23
|
+
buildChartingText,
|
|
24
|
+
buildMinimalCompactionPrompt,
|
|
25
|
+
clearChartingPending,
|
|
26
|
+
createChartingState,
|
|
27
|
+
hasPendingCharting,
|
|
28
|
+
markChartingPending,
|
|
29
|
+
} from "./charting.ts";
|
|
30
|
+
import { readData, runCall, overviewData, browseData, searchData } from "../runtime/runtime.ts";
|
|
31
|
+
import { loadChartingData } from "../runtime/charting.ts";
|
|
32
|
+
import type { SearchInput } from "../runtime/search.ts";
|
|
33
|
+
import type { EngramConfig } from "./config.ts";
|
|
34
|
+
import type { SearchPartType } from "../domain/types.ts";
|
|
35
|
+
|
|
36
|
+
function isUpstreamHistoryDisabledForAgent(
|
|
37
|
+
agentName: string | undefined,
|
|
38
|
+
config: EngramConfig,
|
|
39
|
+
): boolean {
|
|
40
|
+
if (agentName === undefined) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return config.upstream_history.disable_for_agents.includes(agentName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkMessageId(messageID?: string) {
|
|
48
|
+
if (!messageID?.trim()) {
|
|
49
|
+
invalid("message_id is required");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeOptionalMessageId(messageID?: string) {
|
|
54
|
+
if (messageID === undefined) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!messageID.trim()) {
|
|
58
|
+
invalid("message_id is required");
|
|
59
|
+
}
|
|
60
|
+
return messageID.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizePartId(partID?: string) {
|
|
64
|
+
if (!partID?.trim()) {
|
|
65
|
+
invalid("part_id is required");
|
|
66
|
+
}
|
|
67
|
+
return partID.trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkSessionId(sessionID?: string): string {
|
|
71
|
+
const normalized = sessionID?.trim();
|
|
72
|
+
if (!normalized) {
|
|
73
|
+
invalid("session_id is required");
|
|
74
|
+
}
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeOverviewWindowValue(value: number | undefined, name: string) {
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
83
|
+
invalid(`${name} must be a non-negative integer`);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeOverviewTurnIndex(value: number | undefined) {
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
93
|
+
invalid("turn_index must be a non-negative integer");
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// Search Input Validation
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
const searchQueryMaxLength = 500;
|
|
103
|
+
const SEARCH_TYPES = ["text", "tool", "reasoning"] as const satisfies readonly SearchPartType[];
|
|
104
|
+
|
|
105
|
+
function checkSearchQuery(query?: string): string {
|
|
106
|
+
if (query === undefined || query === null) {
|
|
107
|
+
invalid("query is required");
|
|
108
|
+
}
|
|
109
|
+
const normalized = String(query).trim();
|
|
110
|
+
if (normalized.length === 0) {
|
|
111
|
+
invalid("query is required");
|
|
112
|
+
}
|
|
113
|
+
if (normalized.length > searchQueryMaxLength) {
|
|
114
|
+
invalid("query is too long. Use shorter, more specific keywords.");
|
|
115
|
+
}
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeSearchLiteral(literal?: boolean): boolean {
|
|
120
|
+
if (literal === undefined) return false;
|
|
121
|
+
return literal === true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeSearchTypes(value?: string[]): SearchPartType[] {
|
|
125
|
+
if (value === undefined) {
|
|
126
|
+
return ["text"];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
130
|
+
invalid("type must contain at least one of: text, tool, reasoning");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result: SearchPartType[] = [];
|
|
134
|
+
const seen = new Set<SearchPartType>();
|
|
135
|
+
for (const item of value) {
|
|
136
|
+
if (!SEARCH_TYPES.includes(item as SearchPartType)) {
|
|
137
|
+
invalid("type must contain only: text, tool, reasoning");
|
|
138
|
+
}
|
|
139
|
+
const normalized = item as SearchPartType;
|
|
140
|
+
if (seen.has(normalized)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
seen.add(normalized);
|
|
144
|
+
result.push(normalized);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (result.length === 0) {
|
|
148
|
+
invalid("type must contain at least one of: text, tool, reasoning");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildSearchInput(
|
|
155
|
+
query: string,
|
|
156
|
+
literal: boolean,
|
|
157
|
+
types: SearchPartType[],
|
|
158
|
+
config: EngramConfig,
|
|
159
|
+
): SearchInput {
|
|
160
|
+
return {
|
|
161
|
+
query,
|
|
162
|
+
literal,
|
|
163
|
+
limit: config.search.message_limit,
|
|
164
|
+
types,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function logChartingWarning(
|
|
169
|
+
input: PluginInput,
|
|
170
|
+
sessionID: string,
|
|
171
|
+
error: unknown,
|
|
172
|
+
) {
|
|
173
|
+
await input.client.app.log({
|
|
174
|
+
body: {
|
|
175
|
+
service: "engram-plugin",
|
|
176
|
+
level: "warn",
|
|
177
|
+
message: "Failed to inject chart block during compaction",
|
|
178
|
+
extra: {
|
|
179
|
+
sessionID,
|
|
180
|
+
error: error instanceof Error ? error.message : String(error),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
}).catch(() => undefined);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function isCompactionTextCompletion(
|
|
187
|
+
input: PluginInput,
|
|
188
|
+
sessionID: string,
|
|
189
|
+
messageID: string,
|
|
190
|
+
partID: string,
|
|
191
|
+
) {
|
|
192
|
+
const result = await input.client.session.message({
|
|
193
|
+
path: {
|
|
194
|
+
id: sessionID,
|
|
195
|
+
messageID,
|
|
196
|
+
},
|
|
197
|
+
throwOnError: false,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const status = result.response?.status ?? 0;
|
|
201
|
+
if (result.error || status >= 400 || !result.data) {
|
|
202
|
+
throw new Error("Failed to read message. This may be a temporary issue — try again.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const targetPart = result.data.parts.find((part) => part.id === partID);
|
|
206
|
+
if (!targetPart || targetPart.type !== "text") {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result.data.info.role === "assistant" && result.data.info.summary === true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const EngramPlugin: Plugin = async (input) => {
|
|
214
|
+
const upstreamNavigatorState = createUpstreamNavigatorState();
|
|
215
|
+
const historyPromptState = createHistoryPromptState();
|
|
216
|
+
const chartingState = createChartingState();
|
|
217
|
+
const agentNamesBySession = new Map<string, string>();
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
event: async ({ event }) => {
|
|
221
|
+
recordUpstreamNavigatorSession(upstreamNavigatorState, event);
|
|
222
|
+
if (event.type === "session.compacted") {
|
|
223
|
+
clearChartingPending(
|
|
224
|
+
chartingState,
|
|
225
|
+
event.properties.sessionID,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
"chat.message": async (hookInput, output) => {
|
|
230
|
+
agentNamesBySession.set(hookInput.sessionID, output.message.agent);
|
|
231
|
+
},
|
|
232
|
+
"experimental.chat.system.transform": async (hookInput, output) => {
|
|
233
|
+
const config = await loadEngramConfig(input.directory, undefined, input.client);
|
|
234
|
+
|
|
235
|
+
// Upstream navigator prompt injection (only for child sessions with upstream_history enabled)
|
|
236
|
+
if (config.upstream_history.enable) {
|
|
237
|
+
const agentName = hookInput.sessionID
|
|
238
|
+
? agentNamesBySession.get(hookInput.sessionID)
|
|
239
|
+
: undefined;
|
|
240
|
+
if (!isUpstreamHistoryDisabledForAgent(agentName, config)) {
|
|
241
|
+
await injectUpstreamNavigatorPrompt(
|
|
242
|
+
upstreamNavigatorState,
|
|
243
|
+
hookInput.sessionID,
|
|
244
|
+
output.system,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Common history prompt injection (charting or upstream_history enabled)
|
|
250
|
+
if (config.context_charting.enable || config.upstream_history.enable) {
|
|
251
|
+
injectHistoryPrompt(historyPromptState, hookInput.sessionID, output.system);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
"experimental.session.compacting": async (hookInput, output) => {
|
|
255
|
+
const config = await loadEngramConfig(input.directory, undefined, input.client);
|
|
256
|
+
if (!config.context_charting.enable) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
output.prompt = buildMinimalCompactionPrompt();
|
|
260
|
+
markChartingPending(chartingState, hookInput.sessionID);
|
|
261
|
+
},
|
|
262
|
+
"experimental.text.complete": async (hookInput, output) => {
|
|
263
|
+
if (!hasPendingCharting(chartingState, hookInput.sessionID)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const config = await loadEngramConfig(input.directory, undefined, input.client);
|
|
269
|
+
if (!config.context_charting.enable) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const isCompactionText = await isCompactionTextCompletion(
|
|
274
|
+
input,
|
|
275
|
+
hookInput.sessionID,
|
|
276
|
+
hookInput.messageID,
|
|
277
|
+
hookInput.partID,
|
|
278
|
+
);
|
|
279
|
+
if (!isCompactionText) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const data = await loadChartingData(
|
|
284
|
+
input,
|
|
285
|
+
hookInput.sessionID,
|
|
286
|
+
config,
|
|
287
|
+
);
|
|
288
|
+
output.text = buildChartingText(
|
|
289
|
+
hookInput.sessionID,
|
|
290
|
+
data.overview,
|
|
291
|
+
data.latestTurnDetail,
|
|
292
|
+
{
|
|
293
|
+
recentTurns: config.context_charting.recent_turns,
|
|
294
|
+
recentMessages: config.context_charting.recent_messages,
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
clearChartingPending(
|
|
298
|
+
chartingState,
|
|
299
|
+
hookInput.sessionID,
|
|
300
|
+
);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
await logChartingWarning(input, hookInput.sessionID, error);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
tool: {
|
|
306
|
+
history_browse_turns: tool({
|
|
307
|
+
description: `Get a high-level overview of a session's history as turn-indexed summaries
|
|
308
|
+
|
|
309
|
+
USE WHEN:
|
|
310
|
+
- You need to understand the context, goals, or what happened in a session
|
|
311
|
+
- You need to inspect turns around a known turn_index
|
|
312
|
+
|
|
313
|
+
DO NOT USE WHEN:
|
|
314
|
+
- You already know what keywords to look for -> use history_search (overview gives the big picture; search locates specific content by keywords)
|
|
315
|
+
- You need exact message detail -> use history_browse_messages or history_pull_message
|
|
316
|
+
|
|
317
|
+
RETURNS: turn summaries in ascending turn_index order. Each turn includes user preview and message_id, plus assistant preview and total message count`,
|
|
318
|
+
args: {
|
|
319
|
+
session_id: tool.schema
|
|
320
|
+
.string()
|
|
321
|
+
.describe("Target session identifier"),
|
|
322
|
+
turn_index: tool.schema
|
|
323
|
+
.number()
|
|
324
|
+
.optional()
|
|
325
|
+
.describe("Target turn_index. Omit to use the newest visible turn"),
|
|
326
|
+
num_before: tool.schema
|
|
327
|
+
.number()
|
|
328
|
+
.optional()
|
|
329
|
+
.describe("How many older turns before turn_index to include. Omit to exclude"),
|
|
330
|
+
num_after: tool.schema
|
|
331
|
+
.number()
|
|
332
|
+
.optional()
|
|
333
|
+
.describe("How many newer turns after turn_index to include. Omit to exclude"),
|
|
334
|
+
},
|
|
335
|
+
async execute(args, ctx) {
|
|
336
|
+
const sessionID = checkSessionId(args.session_id);
|
|
337
|
+
const turnIndex = normalizeOverviewTurnIndex(args.turn_index);
|
|
338
|
+
const numBefore = normalizeOverviewWindowValue(args.num_before, "num_before");
|
|
339
|
+
const numAfter = normalizeOverviewWindowValue(args.num_after, "num_after");
|
|
340
|
+
|
|
341
|
+
return runCall(
|
|
342
|
+
input,
|
|
343
|
+
ctx,
|
|
344
|
+
"history_browse_turns",
|
|
345
|
+
sessionID,
|
|
346
|
+
{
|
|
347
|
+
session_id: args.session_id,
|
|
348
|
+
turn_index: turnIndex,
|
|
349
|
+
num_before: numBefore,
|
|
350
|
+
num_after: numAfter,
|
|
351
|
+
},
|
|
352
|
+
async (browse, config, journal) => {
|
|
353
|
+
return overviewData(input, browse, config, journal, {
|
|
354
|
+
turnIndex,
|
|
355
|
+
numBefore,
|
|
356
|
+
numAfter,
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
history_browse_messages: tool({
|
|
363
|
+
description: `Browse session history around a specific message. Returns a message window in chronological order.
|
|
364
|
+
|
|
365
|
+
USE WHEN:
|
|
366
|
+
- You need context, facts, or decisions from a session history that you don't currently have
|
|
367
|
+
- The user references prior discussion ("as we discussed", "follow the plan", etc.) not present in this session
|
|
368
|
+
- You want messages before and after a known message_id
|
|
369
|
+
|
|
370
|
+
DO NOT USE WHEN:
|
|
371
|
+
- You don't know which part of history to look at -> use history_browse_turns first to get the big picture
|
|
372
|
+
- You know keywords but not the location -> use history_search (search locates by content; browse navigates by position)
|
|
373
|
+
- You already have a message_id and need full content -> use history_pull_message instead
|
|
374
|
+
|
|
375
|
+
RETURNS: messages[] plus before_message_id / after_message_id anchors for extending the visible window`,
|
|
376
|
+
args: {
|
|
377
|
+
session_id: tool.schema
|
|
378
|
+
.string()
|
|
379
|
+
.describe("Target session identifier"),
|
|
380
|
+
message_id: tool.schema
|
|
381
|
+
.string()
|
|
382
|
+
.optional()
|
|
383
|
+
.describe("Anchor message_id. Omit to use the newest visible message"),
|
|
384
|
+
num_before: tool.schema
|
|
385
|
+
.number()
|
|
386
|
+
.optional()
|
|
387
|
+
.describe("How many older visible messages to include. Omit to exclude"),
|
|
388
|
+
num_after: tool.schema
|
|
389
|
+
.number()
|
|
390
|
+
.optional()
|
|
391
|
+
.describe("How many newer visible messages to include. Omit to exclude"),
|
|
392
|
+
},
|
|
393
|
+
async execute(args, ctx) {
|
|
394
|
+
const messageID = normalizeOptionalMessageId(args.message_id);
|
|
395
|
+
const numBefore = normalizeOverviewWindowValue(args.num_before, "num_before");
|
|
396
|
+
const numAfter = normalizeOverviewWindowValue(args.num_after, "num_after");
|
|
397
|
+
|
|
398
|
+
return runCall(
|
|
399
|
+
input,
|
|
400
|
+
ctx,
|
|
401
|
+
"history_browse_messages",
|
|
402
|
+
checkSessionId(args.session_id),
|
|
403
|
+
{
|
|
404
|
+
session_id: args.session_id,
|
|
405
|
+
message_id: messageID,
|
|
406
|
+
num_before: numBefore,
|
|
407
|
+
num_after: numAfter,
|
|
408
|
+
},
|
|
409
|
+
async (browse, config, journal) => {
|
|
410
|
+
return browseData(
|
|
411
|
+
input,
|
|
412
|
+
browse,
|
|
413
|
+
config,
|
|
414
|
+
journal,
|
|
415
|
+
{
|
|
416
|
+
messageID,
|
|
417
|
+
numBefore,
|
|
418
|
+
numAfter,
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
history_pull_message: tool({
|
|
426
|
+
description: `Read a full message from a session's history.
|
|
427
|
+
|
|
428
|
+
USE WHEN:
|
|
429
|
+
- You have a message_id and need detail beyond the preview
|
|
430
|
+
|
|
431
|
+
DO NOT USE WHEN:
|
|
432
|
+
- You don't have a message_id yet -> use history_browse_messages or history_search to find one first
|
|
433
|
+
- You only need one truncated section -> use history_pull_part instead
|
|
434
|
+
|
|
435
|
+
RETURNS: message metadata including turn_index, plus sections[] in conversation order. Long sections are truncated and include a part_id for follow-up`,
|
|
436
|
+
args: {
|
|
437
|
+
session_id: tool.schema
|
|
438
|
+
.string()
|
|
439
|
+
.describe("Target session identifier"),
|
|
440
|
+
message_id: tool.schema
|
|
441
|
+
.string()
|
|
442
|
+
.describe(
|
|
443
|
+
"Message identifier",
|
|
444
|
+
),
|
|
445
|
+
},
|
|
446
|
+
async execute(args, ctx) {
|
|
447
|
+
const sessionID = checkSessionId(args.session_id);
|
|
448
|
+
checkMessageId(args.message_id);
|
|
449
|
+
const messageID = args.message_id.trim();
|
|
450
|
+
|
|
451
|
+
return runCall(
|
|
452
|
+
input,
|
|
453
|
+
ctx,
|
|
454
|
+
"history_pull_message",
|
|
455
|
+
sessionID,
|
|
456
|
+
{
|
|
457
|
+
session_id: args.session_id,
|
|
458
|
+
message_id: args.message_id,
|
|
459
|
+
},
|
|
460
|
+
async (browse, config, journal) => {
|
|
461
|
+
return readData(
|
|
462
|
+
input,
|
|
463
|
+
browse,
|
|
464
|
+
config,
|
|
465
|
+
messageID,
|
|
466
|
+
undefined,
|
|
467
|
+
journal,
|
|
468
|
+
);
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
},
|
|
472
|
+
}),
|
|
473
|
+
history_pull_part: tool({
|
|
474
|
+
description: `Read the full content of a specific truncated section from a session message.
|
|
475
|
+
|
|
476
|
+
USE WHEN:
|
|
477
|
+
- A prior read returned a truncated section with a part_id and you need the full content
|
|
478
|
+
- A search hit includes a part_id and you want that exact section without reading the whole message
|
|
479
|
+
|
|
480
|
+
DO NOT USE WHEN:
|
|
481
|
+
- You don't have both message_id and part_id yet -> use history_browse_messages, history_pull_message, or history_search first
|
|
482
|
+
- You need the full message context -> use history_pull_message instead
|
|
483
|
+
|
|
484
|
+
RETURNS: full content of that single section only`,
|
|
485
|
+
args: {
|
|
486
|
+
session_id: tool.schema
|
|
487
|
+
.string()
|
|
488
|
+
.describe("Target session identifier"),
|
|
489
|
+
message_id: tool.schema
|
|
490
|
+
.string()
|
|
491
|
+
.describe(
|
|
492
|
+
"Message identifier",
|
|
493
|
+
),
|
|
494
|
+
part_id: tool.schema
|
|
495
|
+
.string()
|
|
496
|
+
.describe(
|
|
497
|
+
"Part identifier from a truncated section",
|
|
498
|
+
),
|
|
499
|
+
},
|
|
500
|
+
async execute(args, ctx) {
|
|
501
|
+
const sessionID = checkSessionId(args.session_id);
|
|
502
|
+
checkMessageId(args.message_id);
|
|
503
|
+
const messageID = args.message_id.trim();
|
|
504
|
+
const partID = normalizePartId(args.part_id);
|
|
505
|
+
|
|
506
|
+
return runCall(
|
|
507
|
+
input,
|
|
508
|
+
ctx,
|
|
509
|
+
"history_pull_part",
|
|
510
|
+
sessionID,
|
|
511
|
+
{
|
|
512
|
+
session_id: args.session_id,
|
|
513
|
+
message_id: args.message_id,
|
|
514
|
+
part_id: args.part_id,
|
|
515
|
+
},
|
|
516
|
+
async (browse, config, journal) => {
|
|
517
|
+
return readData(
|
|
518
|
+
input,
|
|
519
|
+
browse,
|
|
520
|
+
config,
|
|
521
|
+
messageID,
|
|
522
|
+
partID,
|
|
523
|
+
journal,
|
|
524
|
+
);
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
},
|
|
528
|
+
}),
|
|
529
|
+
history_search: tool({
|
|
530
|
+
description: `Search a session's history by keywords. Returns matching messages with context snippets.
|
|
531
|
+
|
|
532
|
+
USE WHEN:
|
|
533
|
+
- You need to find specific information (facts, plans, identifiers, errors, etc.) in a session history
|
|
534
|
+
- You need to check whether specific information exists in a session history
|
|
535
|
+
|
|
536
|
+
DO NOT USE WHEN:
|
|
537
|
+
- You want to scan messages in order or navigate to a specific range -> use history_browse_messages (search locates by content; browse navigates by position)
|
|
538
|
+
|
|
539
|
+
RETURNS: Matching messages grouped by relevance. Each message includes role, turn_index, message_id, and hits[] with context snippets around matches. Search and results can be filtered by content type. Use history_pull_part to expand a specific hit, or history_pull_message to read the full message.`,
|
|
540
|
+
args: {
|
|
541
|
+
session_id: tool.schema
|
|
542
|
+
.string()
|
|
543
|
+
.describe("Target session identifier"),
|
|
544
|
+
query: tool.schema
|
|
545
|
+
.string()
|
|
546
|
+
.describe(
|
|
547
|
+
"Search keywords. Short specific terms work best — an identifier like 'computeTurns' beats a generic word like 'function'",
|
|
548
|
+
),
|
|
549
|
+
literal: tool.schema
|
|
550
|
+
.boolean()
|
|
551
|
+
.optional()
|
|
552
|
+
.describe(
|
|
553
|
+
"If true, match the query as an exact case-sensitive substring. Use for file paths, identifiers, error codes. Default false uses BM25 fulltext search",
|
|
554
|
+
),
|
|
555
|
+
type: tool.schema
|
|
556
|
+
.array(tool.schema.string())
|
|
557
|
+
.optional()
|
|
558
|
+
.describe("Searchable content types to include. One or more of text, tool, reasoning. Default [text]"),
|
|
559
|
+
},
|
|
560
|
+
async execute(args, ctx) {
|
|
561
|
+
return runCall(
|
|
562
|
+
input,
|
|
563
|
+
ctx,
|
|
564
|
+
"history_search",
|
|
565
|
+
checkSessionId(args.session_id),
|
|
566
|
+
{
|
|
567
|
+
session_id: args.session_id,
|
|
568
|
+
query: args.query,
|
|
569
|
+
literal: args.literal,
|
|
570
|
+
type: args.type,
|
|
571
|
+
},
|
|
572
|
+
async (browse, config, journal) => {
|
|
573
|
+
const query = checkSearchQuery(args.query);
|
|
574
|
+
const literal = normalizeSearchLiteral(args.literal);
|
|
575
|
+
const types = normalizeSearchTypes(args.type);
|
|
576
|
+
const searchInput = buildSearchInput(query, literal, types, config);
|
|
577
|
+
|
|
578
|
+
return searchData(
|
|
579
|
+
input,
|
|
580
|
+
browse,
|
|
581
|
+
config,
|
|
582
|
+
searchInput,
|
|
583
|
+
journal,
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
);
|
|
587
|
+
},
|
|
588
|
+
}),
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
export default EngramPlugin;
|