@nextclaw/ncp-agent-runtime 0.2.2 → 0.2.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/dist/index.d.ts +63 -3
- package/dist/index.js +376 -11
- package/package.json +5 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,71 @@
|
|
|
1
|
-
import { NcpContextBuilder, NcpToolRegistry, NcpAgentRunInput, NcpContextPrepareOptions, NcpLLMApiInput, NcpRoundBuffer, NcpToolCallResult, NcpStreamEncoder, NcpAssistantReasoningNormalizationMode, OpenAIChatChunk, NcpEncodeContext, NcpEndpointEvent, NcpTool, NcpToolDefinition, NcpLLMApi, NcpLLMApiOptions, NcpAgentRuntime, NcpAgentConversationStateManager, NcpAgentRunOptions } from '@nextclaw/ncp';
|
|
1
|
+
import { NcpContextBuilder, NcpToolRegistry, NcpAgentRunInput, NcpContextPrepareOptions, NcpLLMApiInput, NcpMessagePart, OpenAIContentPart, NcpRoundBuffer, NcpToolCallResult, NcpStreamEncoder, NcpAssistantReasoningNormalizationMode, OpenAIChatChunk, NcpEncodeContext, NcpEndpointEvent, NcpTool, NcpToolDefinition, NcpLLMApi, NcpLLMApiOptions, NcpAgentRuntime, NcpAgentConversationStateManager, NcpAgentRunOptions } from '@nextclaw/ncp';
|
|
2
2
|
|
|
3
|
+
type StoredAttachmentRecord = {
|
|
4
|
+
id: string;
|
|
5
|
+
uri: string;
|
|
6
|
+
storageKey: string;
|
|
7
|
+
originalName: string;
|
|
8
|
+
storedName: string;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
sizeBytes: number;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
sha256: string;
|
|
13
|
+
};
|
|
14
|
+
type SaveAttachmentParams = {
|
|
15
|
+
fileName: string;
|
|
16
|
+
mimeType?: string | null;
|
|
17
|
+
bytes: Uint8Array;
|
|
18
|
+
createdAt?: Date;
|
|
19
|
+
};
|
|
20
|
+
type AttachmentTextSnapshot = {
|
|
21
|
+
text: string;
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
record: StoredAttachmentRecord;
|
|
24
|
+
};
|
|
25
|
+
declare function isTextLikeAttachment(params: {
|
|
26
|
+
mimeType?: string | null;
|
|
27
|
+
fileName?: string | null;
|
|
28
|
+
}): boolean;
|
|
29
|
+
declare class LocalAttachmentStore {
|
|
30
|
+
readonly rootDir: string;
|
|
31
|
+
constructor(options: {
|
|
32
|
+
rootDir: string;
|
|
33
|
+
});
|
|
34
|
+
saveAttachment(params: SaveAttachmentParams): Promise<StoredAttachmentRecord>;
|
|
35
|
+
getAttachmentByUri(uri: string): StoredAttachmentRecord | null;
|
|
36
|
+
readAttachmentBytes(uri: string): Promise<Buffer | null>;
|
|
37
|
+
readAttachmentBytesSync(uri: string): Buffer | null;
|
|
38
|
+
readTextSnapshotSync(uri: string, options: {
|
|
39
|
+
maxBytes: number;
|
|
40
|
+
}): AttachmentTextSnapshot | null;
|
|
41
|
+
statAttachment(uri: string): Promise<StoredAttachmentRecord | null>;
|
|
42
|
+
resolveContentPath(uri: string): string | null;
|
|
43
|
+
private resolveStorageKeyDirectory;
|
|
44
|
+
}
|
|
45
|
+
declare function buildAttachmentContentPath(params: {
|
|
46
|
+
basePath: string;
|
|
47
|
+
attachmentUri: string;
|
|
48
|
+
}): string;
|
|
49
|
+
|
|
50
|
+
type DefaultNcpContextBuilderOptions = {
|
|
51
|
+
toolRegistry?: NcpToolRegistry;
|
|
52
|
+
attachmentStore?: LocalAttachmentStore | null;
|
|
53
|
+
attachmentTextMaxBytes?: number;
|
|
54
|
+
};
|
|
3
55
|
declare class DefaultNcpContextBuilder implements NcpContextBuilder {
|
|
4
56
|
private readonly toolRegistry?;
|
|
5
|
-
|
|
57
|
+
private readonly attachmentStore?;
|
|
58
|
+
private readonly attachmentTextMaxBytes?;
|
|
59
|
+
constructor(toolRegistry?: NcpToolRegistry);
|
|
60
|
+
constructor(options?: DefaultNcpContextBuilderOptions);
|
|
6
61
|
prepare: (input: NcpAgentRunInput, options?: NcpContextPrepareOptions) => NcpLLMApiInput;
|
|
7
62
|
}
|
|
8
63
|
|
|
64
|
+
declare function buildNcpUserContent(parts: NcpMessagePart[], options?: {
|
|
65
|
+
attachmentStore?: LocalAttachmentStore | null;
|
|
66
|
+
maxTextBytes?: number;
|
|
67
|
+
}): string | OpenAIContentPart[];
|
|
68
|
+
|
|
9
69
|
declare class DefaultNcpRoundBuffer implements NcpRoundBuffer {
|
|
10
70
|
private text;
|
|
11
71
|
private readonly toolCalls;
|
|
@@ -77,4 +137,4 @@ declare class DefaultNcpAgentRuntime implements NcpAgentRuntime {
|
|
|
77
137
|
private tapStream;
|
|
78
138
|
}
|
|
79
139
|
|
|
80
|
-
export { DefaultNcpAgentRuntime, type DefaultNcpAgentRuntimeConfig, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi };
|
|
140
|
+
export { type AttachmentTextSnapshot, DefaultNcpAgentRuntime, type DefaultNcpAgentRuntimeConfig, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi, LocalAttachmentStore, type SaveAttachmentParams, type StoredAttachmentRecord, buildAttachmentContentPath, buildNcpUserContent, isTextLikeAttachment };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,342 @@
|
|
|
1
|
+
// src/attachment-store.ts
|
|
2
|
+
import { createHash, randomUUID } from "crypto";
|
|
3
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
6
|
+
var ATTACHMENT_URI_SCHEME = "attachment://local/";
|
|
7
|
+
function normalizeSegment(value) {
|
|
8
|
+
return value.replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "") || "attachment.bin";
|
|
9
|
+
}
|
|
10
|
+
function normalizeOriginalName(value) {
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : "attachment.bin";
|
|
13
|
+
}
|
|
14
|
+
function normalizeMimeType(value) {
|
|
15
|
+
const normalized = value?.trim().toLowerCase() ?? "";
|
|
16
|
+
return normalized.length > 0 ? normalized : "application/octet-stream";
|
|
17
|
+
}
|
|
18
|
+
function buildAttachmentId() {
|
|
19
|
+
return `att_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
20
|
+
}
|
|
21
|
+
function ensureAttachmentRoot(rootDir) {
|
|
22
|
+
const normalized = rootDir.trim();
|
|
23
|
+
if (!normalized) {
|
|
24
|
+
throw new Error("LocalAttachmentStore requires a non-empty rootDir.");
|
|
25
|
+
}
|
|
26
|
+
return resolve(normalized);
|
|
27
|
+
}
|
|
28
|
+
function buildStorageKey(timestamp, attachmentId) {
|
|
29
|
+
const year = String(timestamp.getFullYear());
|
|
30
|
+
const month = String(timestamp.getMonth() + 1).padStart(2, "0");
|
|
31
|
+
const day = String(timestamp.getDate()).padStart(2, "0");
|
|
32
|
+
return `${year}/${month}/${day}/${attachmentId}`;
|
|
33
|
+
}
|
|
34
|
+
function buildAttachmentUri(storageKey) {
|
|
35
|
+
return `${ATTACHMENT_URI_SCHEME}${storageKey}`;
|
|
36
|
+
}
|
|
37
|
+
function parseAttachmentUri(uri) {
|
|
38
|
+
const normalized = uri.trim();
|
|
39
|
+
if (!normalized.startsWith(ATTACHMENT_URI_SCHEME)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const storageKey = normalized.slice(ATTACHMENT_URI_SCHEME.length).replace(/^\/+/, "").trim();
|
|
43
|
+
return storageKey.length > 0 ? storageKey : null;
|
|
44
|
+
}
|
|
45
|
+
function ensureStorageKey(storageKey) {
|
|
46
|
+
const normalized = storageKey.trim().replace(/^\/+|\/+$/g, "");
|
|
47
|
+
if (!normalized) {
|
|
48
|
+
throw new Error("Attachment storage key must not be empty.");
|
|
49
|
+
}
|
|
50
|
+
const segments = normalized.split("/");
|
|
51
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
52
|
+
throw new Error(`Invalid attachment storage key: ${storageKey}`);
|
|
53
|
+
}
|
|
54
|
+
return segments.join("/");
|
|
55
|
+
}
|
|
56
|
+
function hydrateStoredAttachmentRecord(value) {
|
|
57
|
+
const originalName = normalizeOriginalName(value.originalName);
|
|
58
|
+
return {
|
|
59
|
+
...value,
|
|
60
|
+
originalName,
|
|
61
|
+
storedName: normalizeSegment(value.storedName || originalName),
|
|
62
|
+
mimeType: normalizeMimeType(value.mimeType)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function isTextLikeAttachment(params) {
|
|
66
|
+
const mimeType = normalizeMimeType(params.mimeType);
|
|
67
|
+
if (mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/xml" || mimeType === "text/xml" || mimeType === "application/yaml" || mimeType === "text/yaml" || mimeType === "application/x-yaml" || mimeType === "text/csv") {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const normalizedName = (params.fileName ?? "").trim().toLowerCase();
|
|
71
|
+
return [
|
|
72
|
+
".json",
|
|
73
|
+
".md",
|
|
74
|
+
".txt",
|
|
75
|
+
".csv",
|
|
76
|
+
".xml",
|
|
77
|
+
".yaml",
|
|
78
|
+
".yml",
|
|
79
|
+
".js",
|
|
80
|
+
".mjs",
|
|
81
|
+
".cjs",
|
|
82
|
+
".ts",
|
|
83
|
+
".tsx",
|
|
84
|
+
".jsx",
|
|
85
|
+
".py",
|
|
86
|
+
".rb",
|
|
87
|
+
".go",
|
|
88
|
+
".rs",
|
|
89
|
+
".java",
|
|
90
|
+
".kt",
|
|
91
|
+
".swift",
|
|
92
|
+
".php",
|
|
93
|
+
".css",
|
|
94
|
+
".scss",
|
|
95
|
+
".html",
|
|
96
|
+
".sql",
|
|
97
|
+
".sh"
|
|
98
|
+
].some((suffix) => normalizedName.endsWith(suffix));
|
|
99
|
+
}
|
|
100
|
+
var LocalAttachmentStore = class {
|
|
101
|
+
rootDir;
|
|
102
|
+
constructor(options) {
|
|
103
|
+
this.rootDir = ensureAttachmentRoot(options.rootDir);
|
|
104
|
+
}
|
|
105
|
+
async saveAttachment(params) {
|
|
106
|
+
const createdAt = params.createdAt ?? /* @__PURE__ */ new Date();
|
|
107
|
+
const id = buildAttachmentId();
|
|
108
|
+
const storageKey = buildStorageKey(createdAt, id);
|
|
109
|
+
const attachmentDir = this.resolveStorageKeyDirectory(storageKey);
|
|
110
|
+
const originalName = normalizeOriginalName(params.fileName);
|
|
111
|
+
const storedName = normalizeSegment(originalName);
|
|
112
|
+
const mimeType = normalizeMimeType(params.mimeType);
|
|
113
|
+
const bytes = Buffer.from(params.bytes);
|
|
114
|
+
const record = {
|
|
115
|
+
id,
|
|
116
|
+
uri: buildAttachmentUri(storageKey),
|
|
117
|
+
storageKey,
|
|
118
|
+
originalName,
|
|
119
|
+
storedName,
|
|
120
|
+
mimeType,
|
|
121
|
+
sizeBytes: bytes.length,
|
|
122
|
+
createdAt: createdAt.toISOString(),
|
|
123
|
+
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
124
|
+
};
|
|
125
|
+
await mkdir(attachmentDir, { recursive: true });
|
|
126
|
+
await writeFile(join(attachmentDir, storedName), bytes);
|
|
127
|
+
await writeFile(
|
|
128
|
+
join(attachmentDir, "meta.json"),
|
|
129
|
+
`${JSON.stringify(record)}
|
|
130
|
+
`,
|
|
131
|
+
"utf8"
|
|
132
|
+
);
|
|
133
|
+
return record;
|
|
134
|
+
}
|
|
135
|
+
getAttachmentByUri(uri) {
|
|
136
|
+
const storageKey = parseAttachmentUri(uri);
|
|
137
|
+
if (!storageKey) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const metaPath = join(this.resolveStorageKeyDirectory(storageKey), "meta.json");
|
|
141
|
+
if (!existsSync(metaPath)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const text = readFileSync(metaPath, "utf8");
|
|
145
|
+
return hydrateStoredAttachmentRecord(JSON.parse(text));
|
|
146
|
+
}
|
|
147
|
+
async readAttachmentBytes(uri) {
|
|
148
|
+
const record = this.getAttachmentByUri(uri);
|
|
149
|
+
if (!record) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const filePath = join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
153
|
+
try {
|
|
154
|
+
return await readFile(filePath);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
readAttachmentBytesSync(uri) {
|
|
160
|
+
const record = this.getAttachmentByUri(uri);
|
|
161
|
+
if (!record) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const filePath = join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
165
|
+
return existsSync(filePath) ? readFileSync(filePath) : null;
|
|
166
|
+
}
|
|
167
|
+
readTextSnapshotSync(uri, options) {
|
|
168
|
+
const record = this.getAttachmentByUri(uri);
|
|
169
|
+
if (!record || !isTextLikeAttachment({ mimeType: record.mimeType, fileName: record.originalName })) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const bytes = this.readAttachmentBytesSync(uri);
|
|
173
|
+
if (!bytes) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const truncated = bytes.length > options.maxBytes;
|
|
177
|
+
const snapshotBytes = truncated ? bytes.subarray(0, options.maxBytes) : bytes;
|
|
178
|
+
return {
|
|
179
|
+
text: snapshotBytes.toString("utf8"),
|
|
180
|
+
truncated,
|
|
181
|
+
record
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async statAttachment(uri) {
|
|
185
|
+
const record = this.getAttachmentByUri(uri);
|
|
186
|
+
if (!record) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const filePath = join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
190
|
+
try {
|
|
191
|
+
await stat(filePath);
|
|
192
|
+
return record;
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
resolveContentPath(uri) {
|
|
198
|
+
const record = this.getAttachmentByUri(uri);
|
|
199
|
+
if (!record) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return join(this.resolveStorageKeyDirectory(record.storageKey), record.storedName);
|
|
203
|
+
}
|
|
204
|
+
resolveStorageKeyDirectory(storageKey) {
|
|
205
|
+
return join(this.rootDir, ensureStorageKey(storageKey));
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
function buildAttachmentContentPath(params) {
|
|
209
|
+
const query = new URLSearchParams({ uri: params.attachmentUri });
|
|
210
|
+
return `${params.basePath}?${query.toString()}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/user-content.ts
|
|
214
|
+
var DEFAULT_ATTACHMENT_TEXT_MAX_BYTES = 32 * 1024;
|
|
215
|
+
function readOptionalString(value) {
|
|
216
|
+
if (typeof value !== "string") {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const trimmed = value.trim();
|
|
220
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
221
|
+
}
|
|
222
|
+
function decodeInlineBase64(base64) {
|
|
223
|
+
try {
|
|
224
|
+
return Buffer.from(base64, "base64");
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function formatTextAttachmentBlock(params) {
|
|
230
|
+
const fileName = readOptionalString(params.fileName) ?? "attachment";
|
|
231
|
+
const mimeType = readOptionalString(params.mimeType) ?? "application/octet-stream";
|
|
232
|
+
const suffix = params.truncated ? "\n[Attachment truncated]" : "";
|
|
233
|
+
return `[Attachment: ${fileName}]
|
|
234
|
+
[MIME: ${mimeType}]
|
|
235
|
+
${params.text}${suffix}`;
|
|
236
|
+
}
|
|
237
|
+
function resolveImageDataUrl(part, attachmentStore) {
|
|
238
|
+
const attachmentUri = readOptionalString(part.attachmentUri);
|
|
239
|
+
const mimeType = readOptionalString(part.mimeType);
|
|
240
|
+
if (attachmentUri && mimeType?.startsWith("image/")) {
|
|
241
|
+
const bytes = attachmentStore?.readAttachmentBytesSync(attachmentUri);
|
|
242
|
+
if (bytes) {
|
|
243
|
+
return `data:${mimeType};base64,${bytes.toString("base64")}`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const url = readOptionalString(part.url);
|
|
247
|
+
if (url) {
|
|
248
|
+
return url;
|
|
249
|
+
}
|
|
250
|
+
const contentBase64 = readOptionalString(part.contentBase64);
|
|
251
|
+
if (!mimeType || !contentBase64 || !mimeType.startsWith("image/")) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return `data:${mimeType};base64,${contentBase64}`;
|
|
255
|
+
}
|
|
256
|
+
function resolveTextAttachmentBlock(part, options) {
|
|
257
|
+
const attachmentUri = readOptionalString(part.attachmentUri);
|
|
258
|
+
const fileName = readOptionalString(part.name);
|
|
259
|
+
const mimeType = readOptionalString(part.mimeType);
|
|
260
|
+
const maxTextBytes = options.maxTextBytes ?? DEFAULT_ATTACHMENT_TEXT_MAX_BYTES;
|
|
261
|
+
if (attachmentUri) {
|
|
262
|
+
const snapshot = options.attachmentStore?.readTextSnapshotSync(attachmentUri, {
|
|
263
|
+
maxBytes: maxTextBytes
|
|
264
|
+
});
|
|
265
|
+
if (snapshot) {
|
|
266
|
+
return formatTextAttachmentBlock({
|
|
267
|
+
fileName: snapshot.record.originalName,
|
|
268
|
+
mimeType: snapshot.record.mimeType,
|
|
269
|
+
text: snapshot.text,
|
|
270
|
+
truncated: snapshot.truncated
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const contentBase64 = readOptionalString(part.contentBase64);
|
|
275
|
+
if (!contentBase64 || !isTextLikeAttachment({ mimeType, fileName })) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const bytes = decodeInlineBase64(contentBase64);
|
|
279
|
+
if (!bytes) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const truncated = bytes.length > maxTextBytes;
|
|
283
|
+
const text = (truncated ? bytes.subarray(0, maxTextBytes) : bytes).toString("utf8");
|
|
284
|
+
return formatTextAttachmentBlock({
|
|
285
|
+
fileName,
|
|
286
|
+
mimeType,
|
|
287
|
+
text,
|
|
288
|
+
truncated
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function buildNcpUserContent(parts, options = {}) {
|
|
292
|
+
const content = [];
|
|
293
|
+
for (const part of parts) {
|
|
294
|
+
if ((part.type === "text" || part.type === "rich-text") && part.text.trim().length > 0) {
|
|
295
|
+
content.push({ type: "text", text: part.text });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (part.type !== "file") {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const textAttachmentBlock = resolveTextAttachmentBlock(part, options);
|
|
302
|
+
if (textAttachmentBlock) {
|
|
303
|
+
content.push({ type: "text", text: textAttachmentBlock });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const imageUrl = resolveImageDataUrl(part, options.attachmentStore);
|
|
307
|
+
if (!imageUrl) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
content.push({
|
|
311
|
+
type: "image_url",
|
|
312
|
+
image_url: {
|
|
313
|
+
url: imageUrl
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (content.length === 0) {
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
if (content.length === 1 && content[0]?.type === "text") {
|
|
321
|
+
return content[0].text;
|
|
322
|
+
}
|
|
323
|
+
return content;
|
|
324
|
+
}
|
|
325
|
+
|
|
1
326
|
// src/context-builder.ts
|
|
2
327
|
function isRecord(value) {
|
|
3
328
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4
329
|
}
|
|
5
|
-
function
|
|
330
|
+
function readOptionalString2(value) {
|
|
6
331
|
if (typeof value !== "string") {
|
|
7
332
|
return null;
|
|
8
333
|
}
|
|
9
334
|
const trimmed = value.trim();
|
|
10
335
|
return trimmed.length > 0 ? trimmed : null;
|
|
11
336
|
}
|
|
337
|
+
function isTextLikePart(part) {
|
|
338
|
+
return part.type === "text" || part.type === "rich-text";
|
|
339
|
+
}
|
|
12
340
|
function mergeMessageAndRequestMetadata(input) {
|
|
13
341
|
const messageMetadata = input.messages.slice().reverse().find((message) => isRecord(message.metadata))?.metadata;
|
|
14
342
|
return {
|
|
@@ -23,18 +351,32 @@ function readRequestedToolNames(metadata) {
|
|
|
23
351
|
}
|
|
24
352
|
const deduped = /* @__PURE__ */ new Set();
|
|
25
353
|
for (const item of raw) {
|
|
26
|
-
const value =
|
|
354
|
+
const value = readOptionalString2(item);
|
|
27
355
|
if (value) {
|
|
28
356
|
deduped.add(value);
|
|
29
357
|
}
|
|
30
358
|
}
|
|
31
359
|
return [...deduped];
|
|
32
360
|
}
|
|
33
|
-
function
|
|
361
|
+
function isDefaultNcpContextBuilderOptions(value) {
|
|
362
|
+
return Boolean(value) && typeof value === "object" && !("getToolDefinitions" in value);
|
|
363
|
+
}
|
|
364
|
+
function messageToOpenAI(msg, options) {
|
|
34
365
|
const role = msg.role;
|
|
35
366
|
const parts = msg.parts ?? [];
|
|
36
367
|
if (role === "user" || role === "system") {
|
|
37
|
-
|
|
368
|
+
if (role === "user") {
|
|
369
|
+
return [
|
|
370
|
+
{
|
|
371
|
+
role,
|
|
372
|
+
content: buildNcpUserContent(parts, {
|
|
373
|
+
attachmentStore: options.attachmentStore,
|
|
374
|
+
maxTextBytes: options.attachmentTextMaxBytes
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
const text = parts.filter(isTextLikePart).map((part) => part.text).join("");
|
|
38
380
|
return [{ role, content: text }];
|
|
39
381
|
}
|
|
40
382
|
if (role === "assistant") {
|
|
@@ -93,8 +435,17 @@ function messageToOpenAI(msg) {
|
|
|
93
435
|
return [];
|
|
94
436
|
}
|
|
95
437
|
var DefaultNcpContextBuilder = class {
|
|
96
|
-
|
|
97
|
-
|
|
438
|
+
toolRegistry;
|
|
439
|
+
attachmentStore;
|
|
440
|
+
attachmentTextMaxBytes;
|
|
441
|
+
constructor(toolRegistryOrOptions) {
|
|
442
|
+
if (isDefaultNcpContextBuilderOptions(toolRegistryOrOptions)) {
|
|
443
|
+
this.toolRegistry = toolRegistryOrOptions.toolRegistry;
|
|
444
|
+
this.attachmentStore = toolRegistryOrOptions.attachmentStore;
|
|
445
|
+
this.attachmentTextMaxBytes = toolRegistryOrOptions.attachmentTextMaxBytes;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.toolRegistry = toolRegistryOrOptions;
|
|
98
449
|
}
|
|
99
450
|
prepare = (input, options) => {
|
|
100
451
|
const maxMessages = options?.maxMessages ?? 50;
|
|
@@ -107,10 +458,20 @@ var DefaultNcpContextBuilder = class {
|
|
|
107
458
|
messages.push({ role: "system", content: systemPrompt });
|
|
108
459
|
}
|
|
109
460
|
for (const msg of sessionMessages.slice(-maxMessages)) {
|
|
110
|
-
messages.push(
|
|
461
|
+
messages.push(
|
|
462
|
+
...messageToOpenAI(msg, {
|
|
463
|
+
attachmentStore: this.attachmentStore,
|
|
464
|
+
attachmentTextMaxBytes: this.attachmentTextMaxBytes
|
|
465
|
+
})
|
|
466
|
+
);
|
|
111
467
|
}
|
|
112
468
|
for (const msg of input.messages) {
|
|
113
|
-
messages.push(
|
|
469
|
+
messages.push(
|
|
470
|
+
...messageToOpenAI(msg, {
|
|
471
|
+
attachmentStore: this.attachmentStore,
|
|
472
|
+
attachmentTextMaxBytes: this.attachmentTextMaxBytes
|
|
473
|
+
})
|
|
474
|
+
);
|
|
114
475
|
}
|
|
115
476
|
const toolDefinitions = this.toolRegistry?.getToolDefinitions() ?? [];
|
|
116
477
|
const filteredToolDefinitions = requestedToolNames.length > 0 ? toolDefinitions.filter((definition) => requestedToolNames.includes(definition.name)) : toolDefinitions;
|
|
@@ -125,8 +486,8 @@ var DefaultNcpContextBuilder = class {
|
|
|
125
486
|
return {
|
|
126
487
|
messages,
|
|
127
488
|
tools: tools && tools.length > 0 ? tools : void 0,
|
|
128
|
-
model:
|
|
129
|
-
thinkingLevel:
|
|
489
|
+
model: readOptionalString2(requestMetadata.model) ?? readOptionalString2(requestMetadata.llm_model) ?? readOptionalString2(requestMetadata.agent_model) ?? void 0,
|
|
490
|
+
thinkingLevel: readOptionalString2(requestMetadata.thinking) ?? readOptionalString2(requestMetadata.thinking_level) ?? readOptionalString2(requestMetadata.thinkingLevel) ?? readOptionalString2(requestMetadata.thinking_effort) ?? readOptionalString2(requestMetadata.thinkingEffort) ?? null
|
|
130
491
|
};
|
|
131
492
|
};
|
|
132
493
|
};
|
|
@@ -795,5 +1156,9 @@ export {
|
|
|
795
1156
|
DefaultNcpRoundBuffer,
|
|
796
1157
|
DefaultNcpStreamEncoder,
|
|
797
1158
|
DefaultNcpToolRegistry,
|
|
798
|
-
EchoNcpLLMApi
|
|
1159
|
+
EchoNcpLLMApi,
|
|
1160
|
+
LocalAttachmentStore,
|
|
1161
|
+
buildAttachmentContentPath,
|
|
1162
|
+
buildNcpUserContent,
|
|
1163
|
+
isTextLikeAttachment
|
|
799
1164
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ncp-agent-runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Default agent runtime implementation built on NCP interfaces.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,17 +15,19 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@nextclaw/ncp": "0.3.
|
|
18
|
+
"@nextclaw/ncp": "0.3.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.17.6",
|
|
22
22
|
"prettier": "^3.3.3",
|
|
23
23
|
"tsup": "^8.3.5",
|
|
24
|
-
"typescript": "^5.6.3"
|
|
24
|
+
"typescript": "^5.6.3",
|
|
25
|
+
"vitest": "^2.1.2"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
27
28
|
"build": "tsup src/index.ts --format esm --dts --out-dir dist",
|
|
28
29
|
"lint": "eslint .",
|
|
30
|
+
"test": "vitest run",
|
|
29
31
|
"tsc": "tsc -p tsconfig.json"
|
|
30
32
|
}
|
|
31
33
|
}
|