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,132 @@
|
|
|
1
|
+
import { clipPreviewText } from "./clip-text.ts";
|
|
2
|
+
import { computeToolCalls } from "./domain.ts";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
NormalizedMessage,
|
|
6
|
+
NormalizedPart,
|
|
7
|
+
PreviewFallbackHints,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
|
|
10
|
+
function clipPreviewTextInfo(text: string, maxLength: number) {
|
|
11
|
+
const body = text.replace(/\r?\n/g, " ").trim();
|
|
12
|
+
return {
|
|
13
|
+
preview: clipPreviewText(body, maxLength),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute a preview string from normalized parts.
|
|
19
|
+
*
|
|
20
|
+
* Returns the first visible text part as a single-line clipped preview.
|
|
21
|
+
*/
|
|
22
|
+
export function computePreview(
|
|
23
|
+
parts: NormalizedPart[],
|
|
24
|
+
maxLength: number,
|
|
25
|
+
): string | undefined {
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
if (part.type === "text" && !part.ignored && part.text.trim()) {
|
|
28
|
+
return clipPreviewTextInfo(part.text, maxLength).preview;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute a preview string from the LAST visible text part.
|
|
37
|
+
*
|
|
38
|
+
* Used for assistant overview previews where the conclusion (last text)
|
|
39
|
+
* has higher orientation value than the intent (first text).
|
|
40
|
+
*/
|
|
41
|
+
export function computeLastPreview(
|
|
42
|
+
parts: NormalizedPart[],
|
|
43
|
+
maxLength: number,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
46
|
+
const part = parts[i]!;
|
|
47
|
+
if (part.type === "text" && !part.ignored && part.text.trim()) {
|
|
48
|
+
return clipPreviewTextInfo(part.text, maxLength).preview;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildToolFallbackLabel(parts: NormalizedPart[]) {
|
|
56
|
+
if (computeToolCalls(parts).length === 0) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hasReasoning = parts.some((part) => part.type === "reasoning");
|
|
61
|
+
const hasAttachments = parts.some((part) => part.type === "image" || part.type === "file");
|
|
62
|
+
|
|
63
|
+
if (!hasReasoning && !hasAttachments) {
|
|
64
|
+
return "[tool calls only]";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let prefix = "[tool calls";
|
|
68
|
+
|
|
69
|
+
if (hasReasoning) {
|
|
70
|
+
prefix += " + reasoning";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (hasAttachments) {
|
|
74
|
+
prefix += " + attachments";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `${prefix}]`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Compute a bracketed semantic fallback when no visible text exists.
|
|
82
|
+
*
|
|
83
|
+
* Higher priority means the fallback is more specific and should win when a turn
|
|
84
|
+
* needs to choose among multiple non-text messages for the same role.
|
|
85
|
+
*/
|
|
86
|
+
export function computePreviewFallback(
|
|
87
|
+
msg: NormalizedMessage,
|
|
88
|
+
parts: NormalizedPart[],
|
|
89
|
+
hints: PreviewFallbackHints,
|
|
90
|
+
maxLength: number,
|
|
91
|
+
): { preview: string; priority: number } | undefined {
|
|
92
|
+
function fallback(preview: string, priority: number) {
|
|
93
|
+
return {
|
|
94
|
+
preview: clipPreviewTextInfo(preview, maxLength).preview,
|
|
95
|
+
priority,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (msg.role === "assistant" && msg.summary === true) {
|
|
100
|
+
return fallback("[compacted summary]", 60);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (hints.hasCompaction) {
|
|
104
|
+
return fallback("[compaction trigger]", 50);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const toolLabel = buildToolFallbackLabel(parts);
|
|
108
|
+
if (toolLabel !== undefined) {
|
|
109
|
+
return fallback(toolLabel, 40);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (hints.hasSubtask) {
|
|
113
|
+
return fallback("[subtask request]", 35);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const hasReasoning = parts.some((part) => part.type === "reasoning");
|
|
117
|
+
const hasAttachments = parts.some((part) => part.type === "image" || part.type === "file");
|
|
118
|
+
|
|
119
|
+
if (hasReasoning && hasAttachments) {
|
|
120
|
+
return fallback("[reasoning + attachments]", 30);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (hasReasoning) {
|
|
124
|
+
return fallback("[reasoning only]", 20);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (hasAttachments) {
|
|
128
|
+
return fallback("[attachments only]", 15);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serialize.ts - Response Serialization Layer
|
|
3
|
+
*
|
|
4
|
+
* This module handles the conversion from domain models to final JSON response structures.
|
|
5
|
+
* It is responsible for:
|
|
6
|
+
* - Field naming mappings (camelCase -> snake_case / template field names)
|
|
7
|
+
* - Field omission rules (empty arrays, null values)
|
|
8
|
+
* - Output contract enforcement
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AnyMessageMeta,
|
|
13
|
+
BrowseItemOutput,
|
|
14
|
+
BrowseOutput,
|
|
15
|
+
BrowseUserItemOutput,
|
|
16
|
+
BrowseAssistantItemOutput,
|
|
17
|
+
OverviewTurnOutput,
|
|
18
|
+
OverviewOutput,
|
|
19
|
+
Section,
|
|
20
|
+
SectionOutput,
|
|
21
|
+
ReadablePartType,
|
|
22
|
+
SearchPartType,
|
|
23
|
+
SearchHitOutput,
|
|
24
|
+
SearchMessageOutput,
|
|
25
|
+
SearchOutput,
|
|
26
|
+
} from "./types.ts";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
formatToolCallSummaries,
|
|
30
|
+
} from "./domain.ts";
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Browse Item Serialization
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Serialize a user message metadata into a browse item output.
|
|
38
|
+
*/
|
|
39
|
+
function serializeUserBrowseItem(
|
|
40
|
+
meta: AnyMessageMeta & { role: "user" },
|
|
41
|
+
preview: string | undefined,
|
|
42
|
+
): BrowseUserItemOutput {
|
|
43
|
+
const result: BrowseUserItemOutput = {
|
|
44
|
+
role: "user",
|
|
45
|
+
turn_index: meta.turn,
|
|
46
|
+
message_id: meta.id,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// preview: omit only when no text or semantic fallback is available
|
|
50
|
+
if (preview !== undefined) {
|
|
51
|
+
result.preview = preview;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// attachment: omit if empty
|
|
55
|
+
if (meta.attachments.length > 0) {
|
|
56
|
+
result.attachment = meta.attachments;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Serialize an assistant message metadata into a browse item output.
|
|
64
|
+
*/
|
|
65
|
+
function serializeAssistantBrowseItem(
|
|
66
|
+
meta: AnyMessageMeta & { role: "assistant" },
|
|
67
|
+
preview: string | undefined,
|
|
68
|
+
): BrowseAssistantItemOutput {
|
|
69
|
+
const result: BrowseAssistantItemOutput = {
|
|
70
|
+
role: "assistant",
|
|
71
|
+
turn_index: meta.turn,
|
|
72
|
+
message_id: meta.id,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// preview: omit only when no text or semantic fallback is available
|
|
76
|
+
if (preview !== undefined) {
|
|
77
|
+
result.preview = preview;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// tool: include when there are tool calls
|
|
81
|
+
if (meta.toolCalls.length > 0) {
|
|
82
|
+
result.tool = {
|
|
83
|
+
calls: formatToolCallSummaries(meta.toolCalls),
|
|
84
|
+
outcome: meta.toolOutcome ?? "completed",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Serialize message metadata into a browse item output.
|
|
93
|
+
*
|
|
94
|
+
* @param meta Message metadata (user or assistant)
|
|
95
|
+
* @param preview Preview string or undefined
|
|
96
|
+
* @returns Browse item output with proper field omissions
|
|
97
|
+
*/
|
|
98
|
+
export function serializeBrowseItem(
|
|
99
|
+
meta: AnyMessageMeta,
|
|
100
|
+
preview: string | undefined,
|
|
101
|
+
): BrowseItemOutput {
|
|
102
|
+
if (meta.role === "user") {
|
|
103
|
+
return serializeUserBrowseItem(meta, preview);
|
|
104
|
+
}
|
|
105
|
+
return serializeAssistantBrowseItem(meta, preview);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Serialize a turn summary for overview output.
|
|
110
|
+
*
|
|
111
|
+
* @param turnIndex Turn number
|
|
112
|
+
* @param user Serialized user summary for this turn, or null if hidden/absent
|
|
113
|
+
* @param assistant Serialized assistant summary for this turn, or null if absent
|
|
114
|
+
* @returns Serialized turn output
|
|
115
|
+
*/
|
|
116
|
+
export function serializeOverviewTurn(
|
|
117
|
+
turnIndex: number,
|
|
118
|
+
user: OverviewTurnOutput["user"],
|
|
119
|
+
assistant: OverviewTurnOutput["assistant"],
|
|
120
|
+
): OverviewTurnOutput {
|
|
121
|
+
return {
|
|
122
|
+
turn_index: turnIndex,
|
|
123
|
+
user,
|
|
124
|
+
assistant,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// Overview Tool Serialization
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Serialize a full overview response for the history_browse_turns tool.
|
|
134
|
+
*
|
|
135
|
+
* @param turns Turn summaries in ascending turn order
|
|
136
|
+
* @returns Complete overview output object
|
|
137
|
+
*/
|
|
138
|
+
export function serializeOverview(
|
|
139
|
+
turns: OverviewTurnOutput[],
|
|
140
|
+
): OverviewOutput {
|
|
141
|
+
return {
|
|
142
|
+
turns,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// Full Browse Serialization
|
|
148
|
+
// =============================================================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Serialize a full browse response.
|
|
152
|
+
*
|
|
153
|
+
* @param beforeMessageID message_id of the visible message immediately before this window
|
|
154
|
+
* @param messages Array of browse items
|
|
155
|
+
* @param afterMessageID message_id of the visible message immediately after this window
|
|
156
|
+
* @returns Complete browse output object
|
|
157
|
+
*/
|
|
158
|
+
export function serializeBrowse(
|
|
159
|
+
beforeMessageID: string | null | undefined,
|
|
160
|
+
messages: BrowseItemOutput[],
|
|
161
|
+
afterMessageID: string | null | undefined,
|
|
162
|
+
includeBefore = true,
|
|
163
|
+
includeAfter = true,
|
|
164
|
+
): BrowseOutput {
|
|
165
|
+
const result: BrowseOutput = {
|
|
166
|
+
messages,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (includeBefore && beforeMessageID !== undefined) {
|
|
170
|
+
result.before_message_id = beforeMessageID;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (includeAfter && afterMessageID !== undefined) {
|
|
174
|
+
result.after_message_id = afterMessageID;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// =============================================================================
|
|
181
|
+
// Message Read Serialization
|
|
182
|
+
// =============================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Serialize a section to output format.
|
|
186
|
+
*
|
|
187
|
+
* Rules:
|
|
188
|
+
* - `part_id` only appears when section is truncated (text/reasoning/tool)
|
|
189
|
+
* - `tool.title` is omitted when unavailable
|
|
190
|
+
* - `tool.content` is omitted when hidden or unavailable
|
|
191
|
+
* - image/file sections have no `part_id` or `content`
|
|
192
|
+
*/
|
|
193
|
+
function emptyToNull(value: string | null | undefined) {
|
|
194
|
+
if (value === undefined) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
if (value === null || value.length === 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function serializeSection(section: Section): SectionOutput {
|
|
204
|
+
|
|
205
|
+
switch (section.type) {
|
|
206
|
+
case "text": {
|
|
207
|
+
const output: Extract<SectionOutput, { type: "text" }> = {
|
|
208
|
+
type: "text",
|
|
209
|
+
content: emptyToNull(section.content) ?? null,
|
|
210
|
+
};
|
|
211
|
+
if (section.truncated) {
|
|
212
|
+
output.part_id = section.partId;
|
|
213
|
+
}
|
|
214
|
+
return output;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case "reasoning": {
|
|
218
|
+
const output: Extract<SectionOutput, { type: "reasoning" }> = {
|
|
219
|
+
type: "reasoning",
|
|
220
|
+
content: emptyToNull(section.content) ?? null,
|
|
221
|
+
};
|
|
222
|
+
if (section.truncated) {
|
|
223
|
+
output.part_id = section.partId;
|
|
224
|
+
}
|
|
225
|
+
return output;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case "tool": {
|
|
229
|
+
const output: Extract<SectionOutput, { type: "tool" }> = {
|
|
230
|
+
type: "tool",
|
|
231
|
+
tool: section.tool,
|
|
232
|
+
status: section.status,
|
|
233
|
+
};
|
|
234
|
+
if (section.input !== undefined) {
|
|
235
|
+
output.input = section.input;
|
|
236
|
+
}
|
|
237
|
+
if (section.content === undefined) {
|
|
238
|
+
// omit content
|
|
239
|
+
} else {
|
|
240
|
+
output.content = emptyToNull(section.content) ?? null;
|
|
241
|
+
}
|
|
242
|
+
// part_id only when truncated
|
|
243
|
+
if (section.truncated) {
|
|
244
|
+
output.part_id = section.partId;
|
|
245
|
+
}
|
|
246
|
+
return output;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case "image": {
|
|
250
|
+
return {
|
|
251
|
+
type: "image",
|
|
252
|
+
mime: section.mime,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
case "file": {
|
|
257
|
+
return {
|
|
258
|
+
type: "file",
|
|
259
|
+
path: section.path,
|
|
260
|
+
mime: section.mime,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function toIsoTime(value: unknown): string {
|
|
267
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
268
|
+
return "unknown";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const date = new Date(value);
|
|
272
|
+
if (!Number.isFinite(date.getTime())) {
|
|
273
|
+
return "unknown";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return date.toISOString();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Serialize a message detail for read response.
|
|
281
|
+
*
|
|
282
|
+
* @param meta Message metadata
|
|
283
|
+
* @param sections Array of sections
|
|
284
|
+
* @returns MessageReadOutput as Record for JSON serialization
|
|
285
|
+
*/
|
|
286
|
+
export function serializeMessageRead(
|
|
287
|
+
meta: AnyMessageMeta,
|
|
288
|
+
sections: Section[],
|
|
289
|
+
): Record<string, unknown> {
|
|
290
|
+
const result: Record<string, unknown> = {
|
|
291
|
+
message_id: meta.id,
|
|
292
|
+
role: meta.role,
|
|
293
|
+
turn_index: meta.turn,
|
|
294
|
+
time: toIsoTime(meta.time),
|
|
295
|
+
sections: sections.map((section) => serializeSection(section)),
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// =============================================================================
|
|
302
|
+
// Section Read Serialization
|
|
303
|
+
// =============================================================================
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Serialize a part read response for full content retrieval.
|
|
307
|
+
*
|
|
308
|
+
* @param type Part type
|
|
309
|
+
* @param content Full content (not truncated)
|
|
310
|
+
* @returns PartReadOutput as Record for JSON serialization
|
|
311
|
+
*/
|
|
312
|
+
export function serializePartRead(
|
|
313
|
+
type: ReadablePartType,
|
|
314
|
+
content: string | undefined,
|
|
315
|
+
): Record<string, unknown> {
|
|
316
|
+
const result: Record<string, unknown> = {
|
|
317
|
+
type,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if (content !== undefined) {
|
|
321
|
+
result.content = content;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// =============================================================================
|
|
328
|
+
// Search Serialization (Phase 1: Contracts Only)
|
|
329
|
+
// =============================================================================
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Serialize a single search hit.
|
|
333
|
+
*
|
|
334
|
+
* @param type Part type that matched
|
|
335
|
+
* @param partId Part identifier
|
|
336
|
+
* @param snippets Array of snippet strings
|
|
337
|
+
* @param toolName Optional tool name (only for tool hits)
|
|
338
|
+
* @returns Serialized search hit output
|
|
339
|
+
*/
|
|
340
|
+
export function serializeSearchHit(
|
|
341
|
+
type: SearchPartType,
|
|
342
|
+
partId: string,
|
|
343
|
+
snippets: string[],
|
|
344
|
+
toolName?: string,
|
|
345
|
+
): SearchHitOutput {
|
|
346
|
+
const result: SearchHitOutput = {
|
|
347
|
+
type,
|
|
348
|
+
part_id: partId,
|
|
349
|
+
snippets,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// tool_name only present for tool hits
|
|
353
|
+
if (toolName !== undefined) {
|
|
354
|
+
result.tool_name = toolName;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Serialize a search message result (grouped hits by message).
|
|
362
|
+
*
|
|
363
|
+
* @param messageId Message identifier
|
|
364
|
+
* @param role Message role
|
|
365
|
+
* @param turnIndex Turn number
|
|
366
|
+
* @param hits Array of serialized hits
|
|
367
|
+
* @param remainHits Number of omitted low-priority hits
|
|
368
|
+
* @returns Serialized search message output
|
|
369
|
+
*/
|
|
370
|
+
export function serializeSearchMessage(
|
|
371
|
+
messageId: string,
|
|
372
|
+
role: "user" | "assistant",
|
|
373
|
+
turnIndex: number,
|
|
374
|
+
hits: SearchHitOutput[],
|
|
375
|
+
remainHits: number,
|
|
376
|
+
): SearchMessageOutput {
|
|
377
|
+
const result: SearchMessageOutput = {
|
|
378
|
+
role,
|
|
379
|
+
turn_index: turnIndex,
|
|
380
|
+
message_id: messageId,
|
|
381
|
+
hits,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (remainHits > 0) {
|
|
385
|
+
result.remain_hits = remainHits;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Serialize a full search response.
|
|
393
|
+
*
|
|
394
|
+
* @param messages Array of message results (omit when there are no hits)
|
|
395
|
+
* @returns Complete search output object
|
|
396
|
+
*/
|
|
397
|
+
export function serializeSearch(
|
|
398
|
+
messages: SearchMessageOutput[] | undefined,
|
|
399
|
+
): SearchOutput {
|
|
400
|
+
if (!messages || messages.length === 0) {
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const result: SearchOutput = {
|
|
405
|
+
messages,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return result;
|
|
409
|
+
}
|