@nextclaw/ncp-agent-runtime 0.1.1 → 0.2.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/dist/index.d.ts CHANGED
@@ -67,6 +67,7 @@ declare class DefaultNcpAgentRuntime implements NcpAgentRuntime {
67
67
  * The stream encoder does not emit RunFinished; it only converts chunks to NCP events.
68
68
  */
69
69
  private runLoop;
70
+ private tapStream;
70
71
  }
71
72
 
72
73
  export { DefaultNcpAgentRuntime, type DefaultNcpAgentRuntimeConfig, DefaultNcpContextBuilder, DefaultNcpRoundBuffer, DefaultNcpStreamEncoder, DefaultNcpToolRegistry, EchoNcpLLMApi };
package/dist/index.js CHANGED
@@ -1,4 +1,35 @@
1
1
  // src/context-builder.ts
2
+ function isRecord(value) {
3
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function readOptionalString(value) {
6
+ if (typeof value !== "string") {
7
+ return null;
8
+ }
9
+ const trimmed = value.trim();
10
+ return trimmed.length > 0 ? trimmed : null;
11
+ }
12
+ function mergeMessageAndRequestMetadata(input) {
13
+ const messageMetadata = input.messages.slice().reverse().find((message) => isRecord(message.metadata))?.metadata;
14
+ return {
15
+ ...isRecord(messageMetadata) ? messageMetadata : {},
16
+ ...isRecord(input.metadata) ? input.metadata : {}
17
+ };
18
+ }
19
+ function readRequestedToolNames(metadata) {
20
+ const raw = metadata.requested_tools ?? metadata.requestedTools ?? metadata.requested_skills ?? metadata.requestedSkills;
21
+ if (!Array.isArray(raw)) {
22
+ return [];
23
+ }
24
+ const deduped = /* @__PURE__ */ new Set();
25
+ for (const item of raw) {
26
+ const value = readOptionalString(item);
27
+ if (value) {
28
+ deduped.add(value);
29
+ }
30
+ }
31
+ return [...deduped];
32
+ }
2
33
  function messageToOpenAI(msg) {
3
34
  const role = msg.role;
4
35
  const parts = msg.parts ?? [];
@@ -8,8 +39,12 @@ function messageToOpenAI(msg) {
8
39
  }
9
40
  if (role === "assistant") {
10
41
  const texts = [];
42
+ const reasonings = [];
11
43
  const toolInvocations = [];
12
44
  for (const p of parts) {
45
+ if (p.type === "reasoning") {
46
+ reasonings.push(p.text);
47
+ }
13
48
  if (p.type === "text") {
14
49
  texts.push(p.text);
15
50
  }
@@ -23,11 +58,13 @@ function messageToOpenAI(msg) {
23
58
  }
24
59
  }
25
60
  const text = texts.join("");
61
+ const reasoning = reasonings.join("");
26
62
  const out = [];
27
63
  if (toolInvocations.length > 0) {
28
64
  out.push({
29
65
  role: "assistant",
30
66
  content: text || null,
67
+ ...reasoning ? { reasoning_content: reasoning } : {},
31
68
  tool_calls: toolInvocations.map((t) => ({
32
69
  id: t.toolCallId,
33
70
  type: "function",
@@ -45,7 +82,11 @@ function messageToOpenAI(msg) {
45
82
  });
46
83
  }
47
84
  } else {
48
- out.push({ role: "assistant", content: text });
85
+ out.push({
86
+ role: "assistant",
87
+ content: text,
88
+ ...reasoning ? { reasoning_content: reasoning } : {}
89
+ });
49
90
  }
50
91
  return out;
51
92
  }
@@ -59,6 +100,8 @@ var DefaultNcpContextBuilder = class {
59
100
  const maxMessages = options?.maxMessages ?? 50;
60
101
  const sessionMessages = options?.sessionMessages ?? [];
61
102
  const systemPrompt = options?.systemPrompt;
103
+ const requestMetadata = mergeMessageAndRequestMetadata(input);
104
+ const requestedToolNames = readRequestedToolNames(requestMetadata);
62
105
  const messages = [];
63
106
  if (systemPrompt) {
64
107
  messages.push({ role: "system", content: systemPrompt });
@@ -69,17 +112,21 @@ var DefaultNcpContextBuilder = class {
69
112
  for (const msg of input.messages) {
70
113
  messages.push(...messageToOpenAI(msg));
71
114
  }
72
- const tools = this.toolRegistry ? this.toolRegistry.getToolDefinitions().map((d) => ({
115
+ const toolDefinitions = this.toolRegistry?.getToolDefinitions() ?? [];
116
+ const filteredToolDefinitions = requestedToolNames.length > 0 ? toolDefinitions.filter((definition) => requestedToolNames.includes(definition.name)) : toolDefinitions;
117
+ const tools = filteredToolDefinitions.map((definition) => ({
73
118
  type: "function",
74
119
  function: {
75
- name: d.name,
76
- description: d.description,
77
- parameters: d.parameters
120
+ name: definition.name,
121
+ description: definition.description,
122
+ parameters: definition.parameters
78
123
  }
79
- })) : void 0;
124
+ }));
80
125
  return {
81
126
  messages,
82
- tools: tools && tools.length > 0 ? tools : void 0
127
+ tools: tools && tools.length > 0 ? tools : void 0,
128
+ model: readOptionalString(requestMetadata.model) ?? readOptionalString(requestMetadata.llm_model) ?? readOptionalString(requestMetadata.agent_model) ?? void 0,
129
+ thinkingLevel: readOptionalString(requestMetadata.thinking) ?? readOptionalString(requestMetadata.thinking_level) ?? readOptionalString(requestMetadata.thinkingLevel) ?? readOptionalString(requestMetadata.thinking_effort) ?? readOptionalString(requestMetadata.thinkingEffort) ?? null
83
130
  };
84
131
  };
85
132
  };
@@ -206,9 +253,9 @@ var DefaultNcpStreamEncoder = class {
206
253
  if (!choice) continue;
207
254
  const delta = choice.delta;
208
255
  if (delta) {
256
+ yield* emitReasoningDelta(delta, { sessionId, messageId });
209
257
  const nextState = yield* emitTextDeltas(delta, { sessionId, messageId }, state);
210
258
  state = nextState;
211
- yield* emitReasoningDelta(delta, { sessionId, messageId });
212
259
  yield* emitToolCallDeltas(delta, toolCallBuffers, { sessionId, messageId });
213
260
  }
214
261
  const finishReason = choice.finish_reason;
@@ -296,26 +343,162 @@ import {
296
343
  function genId() {
297
344
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
298
345
  }
299
- function parseToolArgs(args) {
346
+ function stringifyRawArgs(args) {
300
347
  if (typeof args === "string") {
348
+ return args;
349
+ }
350
+ if (args && typeof args === "object" && !Array.isArray(args)) {
301
351
  try {
302
- return JSON.parse(args);
352
+ return JSON.stringify(args);
303
353
  } catch {
304
- return args;
354
+ return "[unserializable-object]";
355
+ }
356
+ }
357
+ return String(args ?? "");
358
+ }
359
+ function parseToolArgs(args) {
360
+ if (args && typeof args === "object" && !Array.isArray(args)) {
361
+ return {
362
+ ok: true,
363
+ rawText: stringifyRawArgs(args),
364
+ value: args
365
+ };
366
+ }
367
+ const rawText = stringifyRawArgs(args);
368
+ if (typeof args !== "string") {
369
+ return {
370
+ ok: false,
371
+ rawText,
372
+ issues: ["Tool arguments must be a JSON object string."]
373
+ };
374
+ }
375
+ const trimmed = args.trim();
376
+ if (!trimmed) {
377
+ return {
378
+ ok: false,
379
+ rawText,
380
+ issues: ["Tool arguments are empty."]
381
+ };
382
+ }
383
+ try {
384
+ const parsed = JSON.parse(trimmed);
385
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
386
+ return {
387
+ ok: false,
388
+ rawText,
389
+ issues: ["Tool arguments JSON must decode to an object."]
390
+ };
391
+ }
392
+ return {
393
+ ok: true,
394
+ rawText,
395
+ value: parsed
396
+ };
397
+ } catch (error) {
398
+ return {
399
+ ok: false,
400
+ rawText,
401
+ issues: [error instanceof Error ? error.message : "Failed to parse tool arguments JSON."]
402
+ };
403
+ }
404
+ }
405
+ function validateToolArgs(args, schema) {
406
+ if (!schema) {
407
+ return [];
408
+ }
409
+ return validateToolValue(args, schema, "");
410
+ }
411
+ function validateToolValue(value, schema, path) {
412
+ const label = path || "parameter";
413
+ const type = typeof schema.type === "string" ? schema.type : void 0;
414
+ if (type && !matchesSchemaType(value, type)) {
415
+ return [`${label} should be ${type}`];
416
+ }
417
+ const errors = [];
418
+ if (schema.enum && !schema.enum.includes(value)) {
419
+ errors.push(`${label} must be one of ${JSON.stringify(schema.enum)}`);
420
+ }
421
+ if (typeof value === "number") {
422
+ if (schema.minimum !== void 0 && value < schema.minimum) {
423
+ errors.push(`${label} must be >= ${schema.minimum}`);
305
424
  }
425
+ if (schema.maximum !== void 0 && value > schema.maximum) {
426
+ errors.push(`${label} must be <= ${schema.maximum}`);
427
+ }
428
+ }
429
+ if (typeof value === "string") {
430
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
431
+ errors.push(`${label} must be at least ${schema.minLength} chars`);
432
+ }
433
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
434
+ errors.push(`${label} must be at most ${schema.maxLength} chars`);
435
+ }
436
+ }
437
+ if (type === "object") {
438
+ const objectValue = value;
439
+ for (const key of schema.required ?? []) {
440
+ if (!(key in objectValue)) {
441
+ errors.push(`missing required ${path ? `${path}.${key}` : key}`);
442
+ }
443
+ }
444
+ const properties = schema.properties ?? {};
445
+ for (const [key, childValue] of Object.entries(objectValue)) {
446
+ const childSchema = properties[key];
447
+ if (!childSchema) {
448
+ continue;
449
+ }
450
+ errors.push(...validateToolValue(childValue, childSchema, path ? `${path}.${key}` : key));
451
+ }
452
+ }
453
+ if (type === "array" && schema.items && Array.isArray(value)) {
454
+ value.forEach((item, index) => {
455
+ errors.push(...validateToolValue(item, schema.items, `${label}[${index}]`));
456
+ });
306
457
  }
307
- return args;
458
+ return errors;
308
459
  }
309
- function appendToolRoundToInput(input, text, toolResults) {
460
+ function matchesSchemaType(value, type) {
461
+ switch (type) {
462
+ case "string":
463
+ return typeof value === "string";
464
+ case "integer":
465
+ return typeof value === "number" && Number.isInteger(value);
466
+ case "number":
467
+ return typeof value === "number";
468
+ case "boolean":
469
+ return typeof value === "boolean";
470
+ case "array":
471
+ return Array.isArray(value);
472
+ case "object":
473
+ return typeof value === "object" && value !== null && !Array.isArray(value);
474
+ default:
475
+ return true;
476
+ }
477
+ }
478
+ function createInvalidToolArgumentsResult(params) {
479
+ return {
480
+ ok: false,
481
+ error: {
482
+ code: "invalid_tool_arguments",
483
+ message: "Tool arguments are invalid.",
484
+ toolCallId: params.toolCallId,
485
+ toolName: params.toolName,
486
+ rawArgumentsText: params.rawArgumentsText,
487
+ issues: params.issues
488
+ }
489
+ };
490
+ }
491
+ function appendToolRoundToInput(input, reasoning, text, toolResults) {
310
492
  const assistantMsg = {
311
493
  role: "assistant",
312
494
  content: text || null,
495
+ ...reasoning ? { reasoning_content: reasoning } : {},
313
496
  tool_calls: toolResults.map((tr) => ({
314
497
  id: tr.toolCallId,
315
498
  type: "function",
316
499
  function: {
317
500
  name: tr.toolName,
318
- arguments: typeof tr.args === "string" ? tr.args : JSON.stringify(tr.args ?? {})
501
+ arguments: tr.rawArgsText
319
502
  }
320
503
  }))
321
504
  };
@@ -330,6 +513,66 @@ function appendToolRoundToInput(input, text, toolResults) {
330
513
  };
331
514
  }
332
515
 
516
+ // src/round-collector.ts
517
+ var DefaultNcpRoundCollector = class {
518
+ text = "";
519
+ reasoning = "";
520
+ toolCallBuffers = /* @__PURE__ */ new Map();
521
+ clear() {
522
+ this.text = "";
523
+ this.reasoning = "";
524
+ this.toolCallBuffers.clear();
525
+ }
526
+ consumeChunk(chunk) {
527
+ const choice = chunk.choices?.[0];
528
+ if (!choice) {
529
+ return;
530
+ }
531
+ const delta = choice.delta;
532
+ if (!delta) {
533
+ return;
534
+ }
535
+ if (typeof delta.content === "string" && delta.content.length > 0) {
536
+ this.text += delta.content;
537
+ }
538
+ const reasoning = delta.reasoning_content ?? delta.reasoning;
539
+ if (typeof reasoning === "string" && reasoning.length > 0) {
540
+ this.reasoning += reasoning;
541
+ }
542
+ const toolDeltas = delta.tool_calls;
543
+ if (!Array.isArray(toolDeltas)) {
544
+ return;
545
+ }
546
+ for (const toolDelta of toolDeltas) {
547
+ const index = getToolCallIndex(toolDelta, this.toolCallBuffers.size);
548
+ const previous = this.toolCallBuffers.get(index) ?? { argumentsText: "" };
549
+ const current = applyToolDelta(previous, toolDelta);
550
+ this.toolCallBuffers.set(index, current);
551
+ }
552
+ }
553
+ getText() {
554
+ return this.text;
555
+ }
556
+ getReasoning() {
557
+ return this.reasoning;
558
+ }
559
+ getToolCalls() {
560
+ const orderedEntries = Array.from(this.toolCallBuffers.entries()).sort(([left], [right]) => left - right);
561
+ const toolCalls = [];
562
+ for (const [, buffer] of orderedEntries) {
563
+ if (!buffer.id || !buffer.name) {
564
+ continue;
565
+ }
566
+ toolCalls.push({
567
+ toolCallId: buffer.id,
568
+ toolName: buffer.name,
569
+ args: buffer.argumentsText
570
+ });
571
+ }
572
+ return toolCalls;
573
+ }
574
+ };
575
+
333
576
  // src/runtime.ts
334
577
  var DefaultNcpAgentRuntime = class {
335
578
  contextBuilder;
@@ -379,55 +622,63 @@ var DefaultNcpAgentRuntime = class {
379
622
  * The stream encoder does not emit RunFinished; it only converts chunks to NCP events.
380
623
  */
381
624
  async *runLoop(llmInput, ctx, options) {
382
- const roundBuffer = new DefaultNcpRoundBuffer();
625
+ const roundCollector = new DefaultNcpRoundCollector();
383
626
  let currentInput = llmInput;
384
627
  let done = false;
385
628
  while (!done && !options?.signal?.aborted) {
386
- roundBuffer.clear();
629
+ roundCollector.clear();
387
630
  const stream = this.llmApi.generate(currentInput, { signal: options?.signal });
388
- for await (const event of this.streamEncoder.encode(stream, ctx)) {
631
+ const tappedStream = this.tapStream(stream, (chunk) => roundCollector.consumeChunk(chunk));
632
+ for await (const event of this.streamEncoder.encode(tappedStream, ctx)) {
389
633
  yield event;
390
- switch (event.type) {
391
- case NcpEventType3.MessageToolCallStart:
392
- roundBuffer.startToolCall(event.payload.toolCallId, event.payload.toolName);
393
- break;
394
- case NcpEventType3.MessageToolCallArgs:
395
- roundBuffer.appendToolCallArgs(event.payload.args);
396
- break;
397
- case NcpEventType3.MessageToolCallEnd: {
398
- const pending = roundBuffer.consumePendingToolCall();
399
- if (pending) {
400
- const parsedArgs = parseToolArgs(pending.args);
401
- const result = await this.toolRegistry.execute(
402
- pending.toolCallId,
403
- pending.toolName,
404
- parsedArgs
405
- );
406
- roundBuffer.appendToolCall({
407
- toolCallId: pending.toolCallId,
408
- toolName: pending.toolName,
409
- args: parsedArgs,
410
- result
411
- });
412
- yield {
413
- type: NcpEventType3.MessageToolCallResult,
414
- payload: {
415
- sessionId: ctx.sessionId,
416
- toolCallId: pending.toolCallId,
417
- content: result
418
- }
419
- };
420
- }
421
- break;
634
+ }
635
+ const toolResults = [];
636
+ for (const toolCall of roundCollector.getToolCalls()) {
637
+ const tool = this.toolRegistry.getTool(toolCall.toolName);
638
+ const parsedArgs = parseToolArgs(toolCall.args);
639
+ let result;
640
+ let args = null;
641
+ if (!parsedArgs.ok) {
642
+ result = createInvalidToolArgumentsResult({
643
+ toolCallId: toolCall.toolCallId,
644
+ toolName: toolCall.toolName,
645
+ rawArgumentsText: parsedArgs.rawText,
646
+ issues: parsedArgs.issues
647
+ });
648
+ } else {
649
+ const schemaIssues = validateToolArgs(parsedArgs.value, tool?.parameters);
650
+ if (schemaIssues.length > 0) {
651
+ result = createInvalidToolArgumentsResult({
652
+ toolCallId: toolCall.toolCallId,
653
+ toolName: toolCall.toolName,
654
+ rawArgumentsText: parsedArgs.rawText,
655
+ issues: schemaIssues
656
+ });
657
+ } else {
658
+ args = parsedArgs.value;
659
+ result = await this.toolRegistry.execute(
660
+ toolCall.toolCallId,
661
+ toolCall.toolName,
662
+ parsedArgs.value
663
+ );
422
664
  }
423
- case NcpEventType3.MessageTextDelta:
424
- roundBuffer.appendText(event.payload.delta);
425
- break;
426
- default:
427
- break;
428
665
  }
666
+ toolResults.push({
667
+ toolCallId: toolCall.toolCallId,
668
+ toolName: toolCall.toolName,
669
+ args,
670
+ rawArgsText: parsedArgs.rawText,
671
+ result
672
+ });
673
+ yield {
674
+ type: NcpEventType3.MessageToolCallResult,
675
+ payload: {
676
+ sessionId: ctx.sessionId,
677
+ toolCallId: toolCall.toolCallId,
678
+ content: result
679
+ }
680
+ };
429
681
  }
430
- const toolResults = roundBuffer.getToolCalls();
431
682
  if (toolResults.length === 0) {
432
683
  yield {
433
684
  type: NcpEventType3.RunFinished,
@@ -438,11 +689,18 @@ var DefaultNcpAgentRuntime = class {
438
689
  }
439
690
  currentInput = appendToolRoundToInput(
440
691
  currentInput,
441
- roundBuffer.getText(),
692
+ roundCollector.getReasoning(),
693
+ roundCollector.getText(),
442
694
  toolResults
443
695
  );
444
696
  }
445
697
  }
698
+ async *tapStream(stream, onChunk) {
699
+ for await (const chunk of stream) {
700
+ onChunk(chunk);
701
+ yield chunk;
702
+ }
703
+ }
446
704
  };
447
705
  export {
448
706
  DefaultNcpAgentRuntime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ncp-agent-runtime",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Default agent runtime implementation built on NCP interfaces.",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@nextclaw/ncp": "0.2.0"
18
+ "@nextclaw/ncp": "0.3.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.17.6",