@nextclaw/ncp-agent-runtime 0.3.13 → 0.3.15

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { NcpAgentConversationStateManager, NcpAgentRunInput, NcpAgentRunOptions, NcpAgentRuntime, NcpAssistantReasoningNormalizationMode, NcpContextBuilder, NcpContextPrepareOptions, NcpEncodeContext, NcpEndpointEvent, NcpInvalidToolArgumentsResult, NcpLLMApi, NcpLLMApiInput, NcpLLMApiOptions, NcpMessagePart, NcpRoundBuffer, NcpStreamEncoder, NcpTool, NcpToolCallResult, NcpToolDefinition, NcpToolRegistry, OpenAIChatChunk, OpenAIContentPart, OpenAITool } from "@nextclaw/ncp";
1
+ import { NcpAgentConversationStateManager, NcpAgentRunInput, NcpAgentRunOptions, NcpAgentRuntime, NcpAssistantReasoningNormalizationMode, NcpContextBuilder, NcpContextPrepareOptions, NcpEncodeContext, NcpEndpointEvent, NcpInvalidToolArgumentsResult, NcpLLMApi, NcpLLMApiInput, NcpLLMApiOptions, NcpMessagePart, NcpRoundBuffer, NcpStreamEncoder, NcpTool, NcpToolCallResult, NcpToolDefinition, NcpToolRegistry, OpenAIChatChunk, OpenAIChatMessage, OpenAIContentPart, OpenAITool } from "@nextclaw/ncp";
2
2
 
3
3
  //#region src/asset-store.d.ts
4
4
  type StoredAssetRecord = {
@@ -142,7 +142,55 @@ declare class EchoNcpLLMApi implements NcpLLMApi {
142
142
  generate: (input: NcpLLMApiInput, options?: NcpLLMApiOptions) => AsyncGenerator<OpenAIChatChunk>;
143
143
  }
144
144
  //#endregion
145
- //#region src/runtime.d.ts
145
+ //#region src/tool-result-content.manager.d.ts
146
+ type ToolResultContentContext = {
147
+ toolCallId?: string;
148
+ toolName?: string;
149
+ };
150
+ type ToolResultContentManagerOptions = {
151
+ maxModelVisibleChars?: number;
152
+ maxToolMessagesChars?: number;
153
+ maxStringValueChars?: number;
154
+ maxModelVisibleImages?: number;
155
+ maxArrayItems?: number;
156
+ maxObjectKeys?: number;
157
+ maxDepth?: number;
158
+ };
159
+ declare class ToolResultContentManager {
160
+ private readonly maxModelVisibleChars;
161
+ private readonly maxToolMessagesChars;
162
+ private readonly maxStringValueChars;
163
+ private readonly maxModelVisibleImages;
164
+ private readonly maxArrayItems;
165
+ private readonly maxObjectKeys;
166
+ private readonly maxDepth;
167
+ private readonly imageService;
168
+ constructor(options?: ToolResultContentManagerOptions);
169
+ normalizeToolCallResult: (result: NcpToolCallResult) => NcpToolCallResult;
170
+ compactInput: (input: NcpLLMApiInput) => NcpLLMApiInput;
171
+ toModelContent: (result: unknown, context?: ToolResultContentContext) => string;
172
+ toVisualObservationMessages: (toolResults: ReadonlyArray<NcpToolCallResult>) => OpenAIChatMessage[];
173
+ private readonly normalizeResult;
174
+ private readonly buildContentItems;
175
+ private readonly readVisibleImages;
176
+ private readonly buildVisualObservationMessage;
177
+ private readonly compactToolMessages;
178
+ private readonly compactToolMessage;
179
+ private readonly normalizeStringResult;
180
+ private readonly redactValue;
181
+ private readonly redactArray;
182
+ private readonly redactObject;
183
+ private readonly redactString;
184
+ private readonly attachNotice;
185
+ private readonly buildEnvelope;
186
+ private readonly buildStringNotice;
187
+ private readonly buildNotice;
188
+ private readonly buildOmittedHistoricalToolResult;
189
+ private readonly boundModelContentText;
190
+ }
191
+ declare const defaultToolResultContentManager: ToolResultContentManager;
192
+ //#endregion
193
+ //#region src/agent-runtime.service.d.ts
146
194
  type DefaultNcpAgentRuntimeConfig = {
147
195
  contextBuilder: NcpContextBuilder;
148
196
  llmApi: NcpLLMApi;
@@ -150,6 +198,7 @@ type DefaultNcpAgentRuntimeConfig = {
150
198
  stateManager: NcpAgentConversationStateManager;
151
199
  streamEncoder?: NcpStreamEncoder;
152
200
  reasoningNormalizationMode?: NcpAssistantReasoningNormalizationMode;
201
+ toolResultContentManager?: ToolResultContentManager;
153
202
  };
154
203
  declare class DefaultNcpAgentRuntime implements NcpAgentRuntime {
155
204
  private readonly contextBuilder;
@@ -158,6 +207,7 @@ declare class DefaultNcpAgentRuntime implements NcpAgentRuntime {
158
207
  private readonly stateManager;
159
208
  private readonly streamEncoder;
160
209
  private readonly reasoningNormalizationMode;
210
+ private readonly toolResultContentManager;
161
211
  constructor(config: DefaultNcpAgentRuntimeConfig);
162
212
  run: (this: DefaultNcpAgentRuntime, input: NcpAgentRunInput, options?: NcpAgentRunOptions) => AsyncGenerator<NcpEndpointEvent>;
163
213
  /**
@@ -210,6 +260,6 @@ declare function createToolExecutionFailedResult(params: {
210
260
  toolName: string;
211
261
  };
212
262
  };
213
- declare function appendToolRoundToInput(input: NcpLLMApiInput, reasoning: string, text: string, toolResults: ReadonlyArray<NcpToolCallResult>): NcpLLMApiInput;
263
+ declare function appendToolRoundToInput(input: NcpLLMApiInput, reasoning: string, text: string, toolResults: ReadonlyArray<NcpToolCallResult>, toolResultContentManager?: ToolResultContentManager): NcpLLMApiInput;
214
264
  //#endregion
215
- export { type AssetMeta, type AssetPutInput, type AssetRef, DefaultNcpAgentRuntime, type DefaultNcpAgentRuntimeConfig, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi, LocalAssetStore, type StoredAssetRecord, appendToolRoundToInput, assertOpenAiFunctionParametersSchema, buildAssetContentPath, buildNcpUserContent, buildOpenAiFunctionTool, createInvalidToolArgumentsResult, createToolExecutionFailedResult, genId, getOpenAiFunctionParametersSchemaIssues, isTextLikeAsset, parseToolArgs, validateToolArgs };
265
+ export { type AssetMeta, type AssetPutInput, type AssetRef, DefaultNcpAgentRuntime, type DefaultNcpAgentRuntimeConfig, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi, LocalAssetStore, type StoredAssetRecord, ToolResultContentManager, type ToolResultContentManagerOptions, appendToolRoundToInput, assertOpenAiFunctionParametersSchema, buildAssetContentPath, buildNcpUserContent, buildOpenAiFunctionTool, createInvalidToolArgumentsResult, createToolExecutionFailedResult, defaultToolResultContentManager, genId, getOpenAiFunctionParametersSchemaIssues, isTextLikeAsset, parseToolArgs, validateToolArgs };
package/dist/index.js CHANGED
@@ -147,6 +147,423 @@ function buildNcpUserContent(parts, options = {}) {
147
147
  return content;
148
148
  }
149
149
  //#endregion
150
+ //#region src/tool-result-content.utils.ts
151
+ const LARGE_INLINE_PAYLOAD_CHARS$1 = 2048;
152
+ function sanitizePositiveInt(value, fallback) {
153
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
154
+ const normalized = Math.floor(value);
155
+ return normalized > 0 ? normalized : fallback;
156
+ }
157
+ function serializeValue(value) {
158
+ if (typeof value === "string") return {
159
+ text: value,
160
+ ok: true
161
+ };
162
+ try {
163
+ return {
164
+ text: JSON.stringify(value ?? null),
165
+ ok: true
166
+ };
167
+ } catch (error) {
168
+ return {
169
+ text: `[unserializable tool result: ${error instanceof Error ? error.message : String(error)}]`,
170
+ ok: false
171
+ };
172
+ }
173
+ }
174
+ function truncateMiddle(value, maxChars) {
175
+ if (value.length <= maxChars) return value;
176
+ if (maxChars <= 0) return "";
177
+ const marker = "\n... [truncated] ...\n";
178
+ if (maxChars <= 21) return value.slice(0, maxChars);
179
+ const keepChars = maxChars - 21;
180
+ const headChars = Math.ceil(keepChars / 2);
181
+ const tailChars = Math.floor(keepChars / 2);
182
+ return `${value.slice(0, headChars)}${marker}${value.slice(value.length - tailChars)}`;
183
+ }
184
+ function isLargeDataUrl(value) {
185
+ return value.length > LARGE_INLINE_PAYLOAD_CHARS$1 && /^data:[^,]+;base64,/i.test(value);
186
+ }
187
+ function readDataUrlMime(value) {
188
+ return /^data:([^;,]+)/i.exec(value)?.[1] ?? "application/octet-stream";
189
+ }
190
+ function isLargeBase64LikeString(value) {
191
+ if (value.length <= LARGE_INLINE_PAYLOAD_CHARS$1) return false;
192
+ const compact = value.replace(/\s+/g, "");
193
+ return compact.length > LARGE_INLINE_PAYLOAD_CHARS$1 && /^[A-Za-z0-9+/=_-]+$/.test(compact);
194
+ }
195
+ function isBinaryLike(value) {
196
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
197
+ }
198
+ function getBinaryByteLength(value) {
199
+ if (value instanceof ArrayBuffer) return value.byteLength;
200
+ if (ArrayBuffer.isView(value)) return value.byteLength;
201
+ return 0;
202
+ }
203
+ function readStringField(value, key) {
204
+ const field = value[key];
205
+ return typeof field === "string" && field.length > 0 ? field : null;
206
+ }
207
+ //#endregion
208
+ //#region src/tool-result-image.service.ts
209
+ const LARGE_INLINE_PAYLOAD_CHARS = 2048;
210
+ const DEFAULT_MAX_MODEL_VISIBLE_IMAGES$1 = 4;
211
+ const DEFAULT_MAX_ARRAY_ITEMS$1 = 50;
212
+ const DEFAULT_MAX_OBJECT_KEYS$1 = 80;
213
+ const DEFAULT_MAX_DEPTH$1 = 8;
214
+ var ToolResultImageService = class {
215
+ maxModelVisibleImages;
216
+ maxArrayItems;
217
+ maxObjectKeys;
218
+ maxDepth;
219
+ constructor(options = {}) {
220
+ this.maxModelVisibleImages = sanitizePositiveInt(options.maxModelVisibleImages, DEFAULT_MAX_MODEL_VISIBLE_IMAGES$1);
221
+ this.maxArrayItems = sanitizePositiveInt(options.maxArrayItems, DEFAULT_MAX_ARRAY_ITEMS$1);
222
+ this.maxObjectKeys = sanitizePositiveInt(options.maxObjectKeys, DEFAULT_MAX_OBJECT_KEYS$1);
223
+ this.maxDepth = sanitizePositiveInt(options.maxDepth, DEFAULT_MAX_DEPTH$1);
224
+ }
225
+ extractImageItems = (value) => {
226
+ const images = [];
227
+ this.collectImageItems(value, images, { depth: 0 });
228
+ return images;
229
+ };
230
+ readVisibleImages = (contentItems) => {
231
+ if (!contentItems) return [];
232
+ return contentItems.filter((item) => item.type === "input_image").slice(0, this.maxModelVisibleImages);
233
+ };
234
+ toOpenAiImagePart = (item) => ({
235
+ type: "image_url",
236
+ image_url: {
237
+ url: item.imageUrl,
238
+ ...item.detail ? { detail: toOpenAiImageDetail(item.detail) } : {}
239
+ }
240
+ });
241
+ summarizeImageContentItem = (value) => {
242
+ const type = readStringField(value, "type");
243
+ if (type === "image") return {
244
+ type: "image",
245
+ mimeType: readStringField(value, "mimeType") ?? readStringField(value, "mime_type"),
246
+ detail: readImageDetail(value),
247
+ dataOmitted: true,
248
+ originalDataChars: readStringField(value, "data")?.length ?? 0
249
+ };
250
+ if (type === "input_image" || type === "image_url") {
251
+ const imageUrl = readImageUrl(value);
252
+ return {
253
+ type,
254
+ imageUrl: imageUrl ? redactImageUrl(imageUrl) : void 0,
255
+ detail: readImageDetail(value)
256
+ };
257
+ }
258
+ return null;
259
+ };
260
+ collectImageItems = (value, output, context) => {
261
+ if (output.length >= this.maxModelVisibleImages || context.depth > this.maxDepth) return;
262
+ if (typeof value === "string") {
263
+ const image = imageDataUrlToContentItem(value);
264
+ if (image) output.push(image);
265
+ return;
266
+ }
267
+ if (!value || typeof value !== "object" || isBinaryLike(value)) return;
268
+ if (Array.isArray(value)) {
269
+ value.slice(0, this.maxArrayItems).forEach((item) => this.collectImageItems(item, output, { depth: context.depth + 1 }));
270
+ return;
271
+ }
272
+ const image = readImageContentItem(value);
273
+ if (image) {
274
+ output.push(image);
275
+ return;
276
+ }
277
+ Object.values(value).slice(0, this.maxObjectKeys).forEach((item) => this.collectImageItems(item, output, { depth: context.depth + 1 }));
278
+ };
279
+ };
280
+ function imageDataUrlToContentItem(value) {
281
+ if (!/^data:image\/[^,]+;base64,/i.test(value)) return null;
282
+ return {
283
+ type: "input_image",
284
+ imageUrl: value,
285
+ mimeType: readDataUrlMime(value),
286
+ originalDataChars: value.length
287
+ };
288
+ }
289
+ function readImageContentItem(value) {
290
+ const type = readStringField(value, "type");
291
+ if (type === "image") {
292
+ const data = readStringField(value, "data");
293
+ const mimeType = readStringField(value, "mimeType") ?? readStringField(value, "mime_type") ?? "image/png";
294
+ if (!data) return null;
295
+ return {
296
+ type: "input_image",
297
+ imageUrl: `data:${mimeType};base64,${data}`,
298
+ mimeType,
299
+ detail: readImageDetail(value),
300
+ originalDataChars: data.length
301
+ };
302
+ }
303
+ if (type === "input_image" || type === "image_url") {
304
+ const imageUrl = readImageUrl(value);
305
+ if (!imageUrl) return null;
306
+ return {
307
+ type: "input_image",
308
+ imageUrl,
309
+ detail: readImageDetail(value)
310
+ };
311
+ }
312
+ return null;
313
+ }
314
+ function toOpenAiImageDetail(detail) {
315
+ if (detail === "low" || detail === "high" || detail === "auto") return detail;
316
+ if (detail === "original") return "high";
317
+ }
318
+ function readImageDetail(value) {
319
+ const direct = readStringField(value, "detail");
320
+ if (isImageDetail(direct)) return direct;
321
+ const meta = value._meta;
322
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return;
323
+ const codexDetail = readStringField(meta, "codex/imageDetail");
324
+ return isImageDetail(codexDetail) ? codexDetail : void 0;
325
+ }
326
+ function isImageDetail(value) {
327
+ return value === "low" || value === "high" || value === "auto" || value === "original";
328
+ }
329
+ function readImageUrl(value) {
330
+ const direct = readStringField(value, "image_url");
331
+ if (direct) return direct;
332
+ const nested = value.image_url;
333
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) return null;
334
+ return readStringField(nested, "url");
335
+ }
336
+ function redactImageUrl(value) {
337
+ if (isLargeDataUrl(value)) return `[omitted data URL payload: mime=${readDataUrlMime(value)}, originalChars=${value.length}]`;
338
+ return value.length > LARGE_INLINE_PAYLOAD_CHARS ? truncateMiddle(value, LARGE_INLINE_PAYLOAD_CHARS) : value;
339
+ }
340
+ //#endregion
341
+ //#region src/tool-result-content.manager.ts
342
+ const DEFAULT_MAX_MODEL_VISIBLE_CHARS = 1e4;
343
+ const DEFAULT_MAX_TOOL_MESSAGES_CHARS = 6e4;
344
+ const DEFAULT_MAX_STRING_VALUE_CHARS = 4e3;
345
+ const DEFAULT_MAX_MODEL_VISIBLE_IMAGES = 4;
346
+ const DEFAULT_MAX_ARRAY_ITEMS = 50;
347
+ const DEFAULT_MAX_OBJECT_KEYS = 80;
348
+ const DEFAULT_MAX_DEPTH = 8;
349
+ const TRUNCATION_MARKER = "[NextClaw tool result truncated]";
350
+ var ToolResultContentManager = class {
351
+ maxModelVisibleChars;
352
+ maxToolMessagesChars;
353
+ maxStringValueChars;
354
+ maxModelVisibleImages;
355
+ maxArrayItems;
356
+ maxObjectKeys;
357
+ maxDepth;
358
+ imageService;
359
+ constructor(options = {}) {
360
+ this.maxModelVisibleChars = sanitizePositiveInt(options.maxModelVisibleChars, DEFAULT_MAX_MODEL_VISIBLE_CHARS);
361
+ this.maxToolMessagesChars = sanitizePositiveInt(options.maxToolMessagesChars, DEFAULT_MAX_TOOL_MESSAGES_CHARS);
362
+ this.maxStringValueChars = sanitizePositiveInt(options.maxStringValueChars, DEFAULT_MAX_STRING_VALUE_CHARS);
363
+ this.maxModelVisibleImages = sanitizePositiveInt(options.maxModelVisibleImages, DEFAULT_MAX_MODEL_VISIBLE_IMAGES);
364
+ this.maxArrayItems = sanitizePositiveInt(options.maxArrayItems, DEFAULT_MAX_ARRAY_ITEMS);
365
+ this.maxObjectKeys = sanitizePositiveInt(options.maxObjectKeys, DEFAULT_MAX_OBJECT_KEYS);
366
+ this.maxDepth = sanitizePositiveInt(options.maxDepth, DEFAULT_MAX_DEPTH);
367
+ this.imageService = new ToolResultImageService({
368
+ maxModelVisibleImages: this.maxModelVisibleImages,
369
+ maxArrayItems: this.maxArrayItems,
370
+ maxObjectKeys: this.maxObjectKeys,
371
+ maxDepth: this.maxDepth
372
+ });
373
+ }
374
+ normalizeToolCallResult = (result) => {
375
+ const context = {
376
+ toolCallId: result.toolCallId,
377
+ toolName: result.toolName
378
+ };
379
+ const normalized = this.normalizeResult(result.result, {
380
+ toolCallId: result.toolCallId,
381
+ toolName: result.toolName
382
+ });
383
+ const contentItems = this.buildContentItems(result.result, normalized.value, context);
384
+ return {
385
+ ...result,
386
+ ...normalized.changed ? { result: normalized.value } : {},
387
+ contentItems
388
+ };
389
+ };
390
+ compactInput = (input) => ({
391
+ ...input,
392
+ messages: this.compactToolMessages(input.messages)
393
+ });
394
+ toModelContent = (result, context = {}) => {
395
+ const serialized = serializeValue(this.normalizeResult(result, context).value);
396
+ if (serialized.text.length <= this.maxModelVisibleChars) return serialized.text;
397
+ return this.boundModelContentText(JSON.stringify(this.buildEnvelope({
398
+ context,
399
+ originalSerializedChars: serialized.text.length,
400
+ previewSource: serialized.text
401
+ })));
402
+ };
403
+ toVisualObservationMessages = (toolResults) => {
404
+ return toolResults.flatMap((result) => {
405
+ const images = this.readVisibleImages(result.contentItems);
406
+ if (images.length === 0) return [];
407
+ return [this.buildVisualObservationMessage(result, images)];
408
+ });
409
+ };
410
+ normalizeResult = (result, context) => {
411
+ if (typeof result === "string") return this.normalizeStringResult(result, context);
412
+ const serialized = serializeValue(result);
413
+ if (serialized.ok && serialized.text.length <= this.maxModelVisibleChars) return {
414
+ value: result,
415
+ changed: false
416
+ };
417
+ const redacted = this.redactValue(result, { depth: 0 });
418
+ const redactedSerialized = serializeValue(redacted);
419
+ if (redactedSerialized.text.length <= this.maxModelVisibleChars) return {
420
+ value: this.attachNotice(redacted, {
421
+ context,
422
+ originalSerializedChars: serialized.text.length
423
+ }),
424
+ changed: true
425
+ };
426
+ return {
427
+ value: this.buildEnvelope({
428
+ context,
429
+ originalSerializedChars: serialized.text.length,
430
+ previewSource: redactedSerialized.text
431
+ }),
432
+ changed: true
433
+ };
434
+ };
435
+ buildContentItems = (rawResult, normalizedResult, context) => {
436
+ return [{
437
+ type: "input_text",
438
+ text: this.toModelContent(normalizedResult, context)
439
+ }, ...this.imageService.extractImageItems(rawResult).slice(0, this.maxModelVisibleImages)];
440
+ };
441
+ readVisibleImages = (contentItems) => this.imageService.readVisibleImages(contentItems);
442
+ buildVisualObservationMessage = (toolResult, images) => {
443
+ const imageParts = images.map(this.imageService.toOpenAiImagePart);
444
+ const omittedCount = Math.max(0, (toolResult.contentItems ?? []).filter((item) => item.type === "input_image").length - imageParts.length);
445
+ return {
446
+ role: "user",
447
+ content: [{
448
+ type: "text",
449
+ text: [
450
+ `Tool "${toolResult.toolName}" returned ${imageParts.length} image(s).`,
451
+ "Use the attached image content as the visual observation for the preceding tool result.",
452
+ omittedCount > 0 ? `${omittedCount} additional image(s) were omitted by budget.` : ""
453
+ ].filter(Boolean).join(" ")
454
+ }, ...imageParts]
455
+ };
456
+ };
457
+ compactToolMessages = (messages) => {
458
+ let remainingChars = this.maxToolMessagesChars;
459
+ const output = messages.slice();
460
+ for (let index = output.length - 1; index >= 0; index -= 1) {
461
+ const message = output[index];
462
+ if (message.role !== "tool") continue;
463
+ const compacted = this.compactToolMessage(message, remainingChars);
464
+ output[index] = compacted.message;
465
+ remainingChars = compacted.remainingChars;
466
+ }
467
+ return output;
468
+ };
469
+ compactToolMessage = (message, remainingChars) => {
470
+ if (message.content.length <= remainingChars) return {
471
+ message,
472
+ remainingChars: remainingChars - message.content.length
473
+ };
474
+ const content = remainingChars >= 1e3 ? truncateMiddle(message.content, remainingChars) : this.buildOmittedHistoricalToolResult(message.content.length);
475
+ return {
476
+ message: {
477
+ ...message,
478
+ content
479
+ },
480
+ remainingChars: Math.max(0, remainingChars - content.length)
481
+ };
482
+ };
483
+ normalizeStringResult = (result, context) => {
484
+ const redacted = this.redactString(result);
485
+ if (redacted.length <= this.maxModelVisibleChars && redacted === result) return {
486
+ value: result,
487
+ changed: false
488
+ };
489
+ if (redacted.length <= this.maxModelVisibleChars) return {
490
+ value: this.buildStringNotice(context, result.length, redacted),
491
+ changed: true
492
+ };
493
+ return {
494
+ value: this.buildStringNotice(context, result.length, truncateMiddle(redacted, this.maxModelVisibleChars)),
495
+ changed: true
496
+ };
497
+ };
498
+ redactValue = (value, context) => {
499
+ if (typeof value === "string") return this.redactString(value);
500
+ if (!value || typeof value !== "object") return value;
501
+ if (value instanceof Date) return value.toISOString();
502
+ const imageSummary = this.imageService.summarizeImageContentItem(value);
503
+ if (imageSummary) return imageSummary;
504
+ if (isBinaryLike(value)) return `[omitted binary payload: bytes=${getBinaryByteLength(value)}]`;
505
+ if (context.depth >= this.maxDepth) return `[omitted nested ${Array.isArray(value) ? "array" : "object"} beyond depth ${this.maxDepth}]`;
506
+ if (Array.isArray(value)) return this.redactArray(value, context);
507
+ return this.redactObject(value, context);
508
+ };
509
+ redactArray = (value, context) => {
510
+ const visibleItems = value.slice(0, this.maxArrayItems).map((item) => this.redactValue(item, { depth: context.depth + 1 }));
511
+ if (value.length <= this.maxArrayItems) return visibleItems;
512
+ return [...visibleItems, `[omitted ${value.length - this.maxArrayItems} array items from tool result]`];
513
+ };
514
+ redactObject = (value, context) => {
515
+ const entries = Object.entries(value);
516
+ const visibleEntries = entries.slice(0, this.maxObjectKeys);
517
+ const output = Object.fromEntries(visibleEntries.map(([key, item]) => [key, this.redactValue(item, { depth: context.depth + 1 })]));
518
+ if (entries.length > this.maxObjectKeys) output._nextclawOmittedKeys = entries.length - this.maxObjectKeys;
519
+ return output;
520
+ };
521
+ redactString = (value) => {
522
+ if (isLargeDataUrl(value)) return `[omitted data URL payload: mime=${readDataUrlMime(value)}, originalChars=${value.length}]`;
523
+ if (isLargeBase64LikeString(value)) return `[omitted base64-like payload: originalChars=${value.length}]`;
524
+ if (value.length <= this.maxStringValueChars) return value;
525
+ return truncateMiddle(value, this.maxStringValueChars);
526
+ };
527
+ attachNotice = (value, params) => {
528
+ const notice = this.buildNotice(params.context, params.originalSerializedChars);
529
+ if (value && typeof value === "object" && !Array.isArray(value)) return {
530
+ ...value,
531
+ _nextclawToolResultNotice: notice
532
+ };
533
+ return {
534
+ type: "nextclaw.tool_result_sanitized",
535
+ notice,
536
+ result: value
537
+ };
538
+ };
539
+ buildEnvelope = (params) => {
540
+ const { context, originalSerializedChars, previewSource } = params;
541
+ return {
542
+ type: "nextclaw.tool_result_truncated",
543
+ notice: this.buildNotice(context, originalSerializedChars),
544
+ originalSerializedChars,
545
+ visibleLimitChars: this.maxModelVisibleChars,
546
+ preview: truncateMiddle(previewSource, Math.max(1e3, this.maxModelVisibleChars - 1e3))
547
+ };
548
+ };
549
+ buildStringNotice = (context, originalChars, preview) => [
550
+ TRUNCATION_MARKER,
551
+ this.buildNotice(context, originalChars),
552
+ "",
553
+ preview
554
+ ].join("\n");
555
+ buildNotice = (context, originalSerializedChars) => {
556
+ const { toolCallId: rawToolCallId, toolName: rawToolName } = context;
557
+ return `${TRUNCATION_MARKER}${rawToolName ? ` tool=${rawToolName}` : ""}${rawToolCallId ? ` toolCallId=${rawToolCallId}` : ""} originalSerializedChars=${originalSerializedChars} visibleLimitChars=${this.maxModelVisibleChars}`;
558
+ };
559
+ buildOmittedHistoricalToolResult = (originalChars) => `${TRUNCATION_MARKER} older tool result omitted from active model context originalChars=${originalChars}`;
560
+ boundModelContentText = (value) => {
561
+ if (value.length <= this.maxModelVisibleChars) return value;
562
+ return truncateMiddle(value, this.maxModelVisibleChars);
563
+ };
564
+ };
565
+ const defaultToolResultContentManager = new ToolResultContentManager();
566
+ //#endregion
150
567
  //#region src/utils.ts
151
568
  const toolSchemaValidator = new AjvPkg({
152
569
  allErrors: true,
@@ -289,7 +706,7 @@ function createToolExecutionFailedResult(params) {
289
706
  }
290
707
  };
291
708
  }
292
- function appendToolRoundToInput(input, reasoning, text, toolResults) {
709
+ function appendToolRoundToInput(input, reasoning, text, toolResults, toolResultContentManager = defaultToolResultContentManager) {
293
710
  const assistantMsg = {
294
711
  role: "assistant",
295
712
  content: text || null,
@@ -305,17 +722,22 @@ function appendToolRoundToInput(input, reasoning, text, toolResults) {
305
722
  };
306
723
  const toolMsgs = toolResults.map((tr) => ({
307
724
  role: "tool",
308
- content: typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result ?? {}),
725
+ content: toolResultContentManager.toModelContent(tr.result, {
726
+ toolCallId: tr.toolCallId,
727
+ toolName: tr.toolName
728
+ }),
309
729
  tool_call_id: tr.toolCallId
310
730
  }));
311
- return {
731
+ const visualMessages = toolResultContentManager.toVisualObservationMessages(toolResults);
732
+ return toolResultContentManager.compactInput({
312
733
  ...input,
313
734
  messages: [
314
735
  ...input.messages,
315
736
  assistantMsg,
316
- ...toolMsgs
737
+ ...toolMsgs,
738
+ ...visualMessages
317
739
  ]
318
- };
740
+ });
319
741
  }
320
742
  //#endregion
321
743
  //#region src/context-builder.ts
@@ -374,7 +796,8 @@ function messageToOpenAI(msg, options) {
374
796
  toolCallId: p.toolCallId ?? "",
375
797
  toolName: p.toolName,
376
798
  args: p.args ?? {},
377
- result: p.result
799
+ result: p.result,
800
+ resultContentItems: p.resultContentItems
378
801
  });
379
802
  }
380
803
  const text = texts.join("");
@@ -399,6 +822,14 @@ function messageToOpenAI(msg, options) {
399
822
  content: typeof t.result === "string" ? t.result : JSON.stringify(t.result),
400
823
  tool_call_id: t.toolCallId
401
824
  });
825
+ out.push(...defaultToolResultContentManager.toVisualObservationMessages(toolInvocations.map((toolInvocation) => ({
826
+ toolCallId: toolInvocation.toolCallId,
827
+ toolName: toolInvocation.toolName,
828
+ args: isRecord(toolInvocation.args) ? toolInvocation.args : null,
829
+ rawArgsText: typeof toolInvocation.args === "string" ? toolInvocation.args : JSON.stringify(toolInvocation.args ?? {}),
830
+ result: toolInvocation.result,
831
+ contentItems: toolInvocation.resultContentItems
832
+ }))));
402
833
  } else out.push({
403
834
  role: "assistant",
404
835
  content: text,
@@ -456,6 +887,37 @@ function normalizeMimeType(value) {
456
887
  const normalized = value?.trim().toLowerCase() ?? "";
457
888
  return normalized.length > 0 ? normalized : "application/octet-stream";
458
889
  }
890
+ function inferMimeTypeFromFileName(fileName) {
891
+ const normalized = fileName.trim().toLowerCase();
892
+ if (normalized.endsWith(".mp3")) return "audio/mpeg";
893
+ if (normalized.endsWith(".m4a")) return "audio/mp4";
894
+ if (normalized.endsWith(".wav")) return "audio/wav";
895
+ if (normalized.endsWith(".ogg")) return "audio/ogg";
896
+ if (normalized.endsWith(".opus")) return "audio/opus";
897
+ if (normalized.endsWith(".flac")) return "audio/flac";
898
+ if (normalized.endsWith(".aac")) return "audio/aac";
899
+ if (normalized.endsWith(".weba")) return "audio/webm";
900
+ if (normalized.endsWith(".mp4") || normalized.endsWith(".m4v")) return "video/mp4";
901
+ if (normalized.endsWith(".mov")) return "video/quicktime";
902
+ if (normalized.endsWith(".webm")) return "video/webm";
903
+ if (normalized.endsWith(".mkv")) return "video/x-matroska";
904
+ if (normalized.endsWith(".avi")) return "video/x-msvideo";
905
+ if (normalized.endsWith(".wmv")) return "video/x-ms-wmv";
906
+ if (normalized.endsWith(".png")) return "image/png";
907
+ if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg";
908
+ if (normalized.endsWith(".gif")) return "image/gif";
909
+ if (normalized.endsWith(".webp")) return "image/webp";
910
+ if (normalized.endsWith(".svg")) return "image/svg+xml";
911
+ if (normalized.endsWith(".pdf")) return "application/pdf";
912
+ if (normalized.endsWith(".json")) return "application/json";
913
+ if (normalized.endsWith(".txt")) return "text/plain";
914
+ return null;
915
+ }
916
+ function resolveStoredMimeType(fileName, mimeType) {
917
+ const normalized = normalizeMimeType(mimeType);
918
+ if (normalized !== "application/octet-stream") return normalized;
919
+ return inferMimeTypeFromFileName(fileName) ?? normalized;
920
+ }
459
921
  function buildAssetId() {
460
922
  return `asset_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
461
923
  }
@@ -616,12 +1078,13 @@ var LocalAssetStore = class {
616
1078
  return record;
617
1079
  }
618
1080
  async putFromBytes(params) {
619
- const bytes = Buffer.from(params.bytes);
1081
+ const { bytes: rawBytes, createdAt, fileName, mimeType } = params;
1082
+ const bytes = Buffer.from(rawBytes);
620
1083
  const record = this.buildRecord({
621
- fileName: params.fileName,
622
- mimeType: params.mimeType,
1084
+ fileName,
1085
+ mimeType,
623
1086
  bytes,
624
- createdAt: params.createdAt
1087
+ createdAt
625
1088
  });
626
1089
  const assetDir = this.resolveStorageKeyDirectory(record.storageKey);
627
1090
  await mkdir(assetDir, { recursive: true });
@@ -630,20 +1093,21 @@ var LocalAssetStore = class {
630
1093
  return record;
631
1094
  }
632
1095
  buildRecord(params) {
633
- const createdAt = params.createdAt ?? /* @__PURE__ */ new Date();
1096
+ const { bytes, createdAt: rawCreatedAt, fileName: rawFileName, mimeType } = params;
1097
+ const createdAt = rawCreatedAt ?? /* @__PURE__ */ new Date();
634
1098
  const id = buildAssetId();
635
1099
  const storageKey = buildStorageKey(createdAt, id);
636
- const fileName = normalizeFileName(params.fileName);
1100
+ const fileName = normalizeFileName(rawFileName);
637
1101
  return {
638
1102
  id,
639
1103
  uri: buildAssetUri(storageKey),
640
1104
  storageKey,
641
1105
  fileName,
642
1106
  storedName: normalizeSegment(fileName),
643
- mimeType: normalizeMimeType(params.mimeType),
644
- sizeBytes: params.bytes.length,
1107
+ mimeType: resolveStoredMimeType(fileName, mimeType),
1108
+ sizeBytes: bytes.length,
645
1109
  createdAt: createdAt.toISOString(),
646
- sha256: createHash("sha256").update(params.bytes).digest("hex")
1110
+ sha256: createHash("sha256").update(bytes).digest("hex")
647
1111
  };
648
1112
  }
649
1113
  async writeMeta(assetDir, record) {
@@ -1094,7 +1558,7 @@ var DefaultNcpRoundCollector = class {
1094
1558
  }
1095
1559
  };
1096
1560
  //#endregion
1097
- //#region src/runtime.ts
1561
+ //#region src/agent-runtime.service.ts
1098
1562
  var DefaultNcpAgentRuntime = class {
1099
1563
  contextBuilder;
1100
1564
  llmApi;
@@ -1102,12 +1566,14 @@ var DefaultNcpAgentRuntime = class {
1102
1566
  stateManager;
1103
1567
  streamEncoder;
1104
1568
  reasoningNormalizationMode;
1569
+ toolResultContentManager;
1105
1570
  constructor(config) {
1106
1571
  this.contextBuilder = config.contextBuilder;
1107
1572
  this.llmApi = config.llmApi;
1108
1573
  this.toolRegistry = config.toolRegistry;
1109
1574
  this.stateManager = config.stateManager;
1110
1575
  this.reasoningNormalizationMode = config.reasoningNormalizationMode ?? "off";
1576
+ this.toolResultContentManager = config.toolResultContentManager ?? defaultToolResultContentManager;
1111
1577
  this.streamEncoder = config.streamEncoder ?? new DefaultNcpStreamEncoder({ reasoningNormalizationMode: this.reasoningNormalizationMode });
1112
1578
  }
1113
1579
  run = async function* (input, options) {
@@ -1161,14 +1627,15 @@ var DefaultNcpAgentRuntime = class {
1161
1627
  for await (const event of this.streamEncoder.encode(tappedStream, ctx)) yield event;
1162
1628
  const toolResults = [];
1163
1629
  for (const toolCall of roundCollector.getToolCalls()) {
1164
- const toolResult = await this.executeToolCall(toolCall);
1630
+ const toolResult = this.toolResultContentManager.normalizeToolCallResult(await this.executeToolCall(toolCall));
1165
1631
  toolResults.push(toolResult);
1166
1632
  yield {
1167
1633
  type: NcpEventType.MessageToolCallResult,
1168
1634
  payload: {
1169
1635
  sessionId: ctx.sessionId,
1170
1636
  toolCallId: toolCall.toolCallId,
1171
- content: toolResult.result
1637
+ content: toolResult.result,
1638
+ contentItems: toolResult.contentItems
1172
1639
  }
1173
1640
  };
1174
1641
  }
@@ -1184,7 +1651,7 @@ var DefaultNcpAgentRuntime = class {
1184
1651
  done = true;
1185
1652
  break;
1186
1653
  }
1187
- currentInput = appendToolRoundToInput(currentInput, roundCollector.getReasoning(), roundCollector.getText(), toolResults);
1654
+ currentInput = appendToolRoundToInput(currentInput, roundCollector.getReasoning(), roundCollector.getText(), toolResults, this.toolResultContentManager);
1188
1655
  }
1189
1656
  };
1190
1657
  executeToolCall = async function(toolCall) {
@@ -1247,4 +1714,4 @@ var DefaultNcpAgentRuntime = class {
1247
1714
  };
1248
1715
  };
1249
1716
  //#endregion
1250
- export { DefaultNcpAgentRuntime, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi, LocalAssetStore, appendToolRoundToInput, assertOpenAiFunctionParametersSchema, buildAssetContentPath, buildNcpUserContent, buildOpenAiFunctionTool, createInvalidToolArgumentsResult, createToolExecutionFailedResult, genId, getOpenAiFunctionParametersSchemaIssues, isTextLikeAsset, parseToolArgs, validateToolArgs };
1717
+ export { DefaultNcpAgentRuntime, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi, LocalAssetStore, ToolResultContentManager, appendToolRoundToInput, assertOpenAiFunctionParametersSchema, buildAssetContentPath, buildNcpUserContent, buildOpenAiFunctionTool, createInvalidToolArgumentsResult, createToolExecutionFailedResult, defaultToolResultContentManager, genId, getOpenAiFunctionParametersSchemaIssues, isTextLikeAsset, parseToolArgs, validateToolArgs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ncp-agent-runtime",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "private": false,
5
5
  "description": "Default agent runtime implementation built on NCP interfaces.",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "dependencies": {
18
18
  "ajv": "^8.17.1",
19
- "@nextclaw/ncp": "0.5.3"
19
+ "@nextclaw/ncp": "0.5.5"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^20.17.6",