@nextclaw/ncp-agent-runtime 0.1.1 → 0.2.1

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 { NcpContextBuilder, NcpToolRegistry, NcpAgentRunInput, NcpContextPrepareOptions, NcpLLMApiInput, NcpRoundBuffer, NcpToolCallResult, NcpStreamEncoder, OpenAIChatChunk, NcpEncodeContext, NcpEndpointEvent, NcpTool, NcpToolDefinition, NcpLLMApi, NcpLLMApiOptions, NcpAgentRuntime, NcpAgentConversationStateManager, NcpAgentRunOptions } from '@nextclaw/ncp';
1
+ import { NcpContextBuilder, NcpToolRegistry, NcpAgentRunInput, NcpContextPrepareOptions, NcpLLMApiInput, NcpRoundBuffer, NcpToolCallResult, NcpStreamEncoder, NcpAssistantReasoningNormalizationMode, OpenAIChatChunk, NcpEncodeContext, NcpEndpointEvent, NcpTool, NcpToolDefinition, NcpLLMApi, NcpLLMApiOptions, NcpAgentRuntime, NcpAgentConversationStateManager, NcpAgentRunOptions } from '@nextclaw/ncp';
2
2
 
3
3
  declare class DefaultNcpContextBuilder implements NcpContextBuilder {
4
4
  private readonly toolRegistry?;
@@ -24,12 +24,17 @@ declare class DefaultNcpRoundBuffer implements NcpRoundBuffer {
24
24
  clear: () => void;
25
25
  }
26
26
 
27
+ type DefaultNcpStreamEncoderConfig = {
28
+ reasoningNormalizationMode?: NcpAssistantReasoningNormalizationMode;
29
+ };
27
30
  /**
28
31
  * Converts LLM stream chunks to NCP events (text, reasoning, tool calls).
29
32
  * Does not emit RunFinished; that is the runtime's responsibility after the loop completes.
30
33
  */
31
34
  declare class DefaultNcpStreamEncoder implements NcpStreamEncoder {
32
- encode: (stream: AsyncIterable<OpenAIChatChunk>, context: NcpEncodeContext) => AsyncGenerator<NcpEndpointEvent>;
35
+ private readonly reasoningNormalizationMode;
36
+ constructor(config?: DefaultNcpStreamEncoderConfig);
37
+ encode(stream: AsyncIterable<OpenAIChatChunk>, context: NcpEncodeContext): AsyncGenerator<NcpEndpointEvent>;
33
38
  }
34
39
 
35
40
  declare class DefaultNcpToolRegistry implements NcpToolRegistry {
@@ -52,6 +57,7 @@ type DefaultNcpAgentRuntimeConfig = {
52
57
  toolRegistry: NcpToolRegistry;
53
58
  stateManager: NcpAgentConversationStateManager;
54
59
  streamEncoder?: NcpStreamEncoder;
60
+ reasoningNormalizationMode?: NcpAssistantReasoningNormalizationMode;
55
61
  };
56
62
  declare class DefaultNcpAgentRuntime implements NcpAgentRuntime {
57
63
  private readonly contextBuilder;
@@ -59,6 +65,7 @@ declare class DefaultNcpAgentRuntime implements NcpAgentRuntime {
59
65
  private readonly toolRegistry;
60
66
  private readonly stateManager;
61
67
  private readonly streamEncoder;
68
+ private readonly reasoningNormalizationMode;
62
69
  constructor(config: DefaultNcpAgentRuntimeConfig);
63
70
  run: (this: DefaultNcpAgentRuntime, input: NcpAgentRunInput, options?: NcpAgentRunOptions) => AsyncGenerator<NcpEndpointEvent>;
64
71
  /**
@@ -67,6 +74,7 @@ declare class DefaultNcpAgentRuntime implements NcpAgentRuntime {
67
74
  * The stream encoder does not emit RunFinished; it only converts chunks to NCP events.
68
75
  */
69
76
  private runLoop;
77
+ private tapStream;
70
78
  }
71
79
 
72
80
  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
  };
@@ -125,6 +172,9 @@ import {
125
172
  } from "@nextclaw/ncp";
126
173
 
127
174
  // src/stream-encoder.utils.ts
175
+ import {
176
+ NcpAssistantTextStreamNormalizer
177
+ } from "@nextclaw/ncp";
128
178
  import { NcpEventType } from "@nextclaw/ncp";
129
179
  function getToolCallIndex(toolDelta, fallback) {
130
180
  const idx = toolDelta.index;
@@ -149,14 +199,68 @@ function applyToolDelta(current, toolDelta) {
149
199
  function* emitTextDeltas(delta, ctx, state) {
150
200
  const content = delta.content;
151
201
  if (typeof content !== "string" || content.length === 0) return state;
202
+ if (state.normalizer) {
203
+ let textStarted = state.textStarted;
204
+ for (const segment of state.normalizer.push(content)) {
205
+ if (segment.type === "reasoning") {
206
+ yield {
207
+ type: NcpEventType.MessageReasoningDelta,
208
+ payload: { ...ctx, delta: segment.text }
209
+ };
210
+ continue;
211
+ }
212
+ if (!textStarted) {
213
+ yield { type: NcpEventType.MessageTextStart, payload: ctx };
214
+ textStarted = true;
215
+ }
216
+ yield {
217
+ type: NcpEventType.MessageTextDelta,
218
+ payload: { ...ctx, delta: segment.text }
219
+ };
220
+ }
221
+ return {
222
+ ...state,
223
+ textStarted
224
+ };
225
+ }
152
226
  if (!state.textStarted) {
153
227
  yield { type: NcpEventType.MessageTextStart, payload: ctx };
154
228
  yield { type: NcpEventType.MessageTextDelta, payload: { ...ctx, delta: content } };
155
- return { textStarted: true };
229
+ return {
230
+ ...state,
231
+ textStarted: true
232
+ };
156
233
  }
157
234
  yield { type: NcpEventType.MessageTextDelta, payload: { ...ctx, delta: content } };
158
235
  return state;
159
236
  }
237
+ function* flushTextDeltas(ctx, state) {
238
+ if (!state.normalizer) {
239
+ return state;
240
+ }
241
+ let textStarted = state.textStarted;
242
+ for (const segment of state.normalizer.finish()) {
243
+ if (segment.type === "reasoning") {
244
+ yield {
245
+ type: NcpEventType.MessageReasoningDelta,
246
+ payload: { ...ctx, delta: segment.text }
247
+ };
248
+ continue;
249
+ }
250
+ if (!textStarted) {
251
+ yield { type: NcpEventType.MessageTextStart, payload: ctx };
252
+ textStarted = true;
253
+ }
254
+ yield {
255
+ type: NcpEventType.MessageTextDelta,
256
+ payload: { ...ctx, delta: segment.text }
257
+ };
258
+ }
259
+ return {
260
+ ...state,
261
+ textStarted
262
+ };
263
+ }
160
264
  function* emitReasoningDelta(delta, ctx) {
161
265
  const reasoning = delta.reasoning_content ?? delta.reasoning;
162
266
  if (typeof reasoning !== "string" || !reasoning) return;
@@ -194,32 +298,43 @@ function* flushToolCalls(buffers, ctx) {
194
298
  };
195
299
  }
196
300
  }
301
+ function createStreamContentState(reasoningNormalizationMode) {
302
+ return {
303
+ textStarted: false,
304
+ normalizer: reasoningNormalizationMode === "think-tags" ? new NcpAssistantTextStreamNormalizer(reasoningNormalizationMode) : null
305
+ };
306
+ }
197
307
 
198
308
  // src/stream-encoder.ts
199
309
  var DefaultNcpStreamEncoder = class {
200
- encode = async function* (stream, context) {
310
+ reasoningNormalizationMode;
311
+ constructor(config = {}) {
312
+ this.reasoningNormalizationMode = config.reasoningNormalizationMode ?? "off";
313
+ }
314
+ async *encode(stream, context) {
201
315
  const { sessionId, messageId } = context;
202
- let state = { textStarted: false };
316
+ let state = createStreamContentState(this.reasoningNormalizationMode);
203
317
  const toolCallBuffers = /* @__PURE__ */ new Map();
204
318
  for await (const chunk of stream) {
205
319
  const choice = chunk.choices?.[0];
206
320
  if (!choice) continue;
207
321
  const delta = choice.delta;
208
322
  if (delta) {
323
+ yield* emitReasoningDelta(delta, { sessionId, messageId });
209
324
  const nextState = yield* emitTextDeltas(delta, { sessionId, messageId }, state);
210
325
  state = nextState;
211
- yield* emitReasoningDelta(delta, { sessionId, messageId });
212
326
  yield* emitToolCallDeltas(delta, toolCallBuffers, { sessionId, messageId });
213
327
  }
214
328
  const finishReason = choice.finish_reason;
215
329
  if (typeof finishReason === "string" && finishReason.trim().length > 0) {
330
+ state = yield* flushTextDeltas({ sessionId, messageId }, state);
216
331
  yield* flushToolCalls(toolCallBuffers, { sessionId, messageId });
217
332
  if (state.textStarted) {
218
333
  yield { type: NcpEventType2.MessageTextEnd, payload: { sessionId, messageId } };
219
334
  }
220
335
  }
221
336
  }
222
- };
337
+ }
223
338
  };
224
339
 
225
340
  // src/tool-registry.ts
@@ -296,26 +411,162 @@ import {
296
411
  function genId() {
297
412
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
298
413
  }
299
- function parseToolArgs(args) {
414
+ function stringifyRawArgs(args) {
300
415
  if (typeof args === "string") {
416
+ return args;
417
+ }
418
+ if (args && typeof args === "object" && !Array.isArray(args)) {
301
419
  try {
302
- return JSON.parse(args);
420
+ return JSON.stringify(args);
303
421
  } catch {
304
- return args;
422
+ return "[unserializable-object]";
305
423
  }
306
424
  }
307
- return args;
425
+ return String(args ?? "");
308
426
  }
309
- function appendToolRoundToInput(input, text, toolResults) {
427
+ function parseToolArgs(args) {
428
+ if (args && typeof args === "object" && !Array.isArray(args)) {
429
+ return {
430
+ ok: true,
431
+ rawText: stringifyRawArgs(args),
432
+ value: args
433
+ };
434
+ }
435
+ const rawText = stringifyRawArgs(args);
436
+ if (typeof args !== "string") {
437
+ return {
438
+ ok: false,
439
+ rawText,
440
+ issues: ["Tool arguments must be a JSON object string."]
441
+ };
442
+ }
443
+ const trimmed = args.trim();
444
+ if (!trimmed) {
445
+ return {
446
+ ok: false,
447
+ rawText,
448
+ issues: ["Tool arguments are empty."]
449
+ };
450
+ }
451
+ try {
452
+ const parsed = JSON.parse(trimmed);
453
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
454
+ return {
455
+ ok: false,
456
+ rawText,
457
+ issues: ["Tool arguments JSON must decode to an object."]
458
+ };
459
+ }
460
+ return {
461
+ ok: true,
462
+ rawText,
463
+ value: parsed
464
+ };
465
+ } catch (error) {
466
+ return {
467
+ ok: false,
468
+ rawText,
469
+ issues: [error instanceof Error ? error.message : "Failed to parse tool arguments JSON."]
470
+ };
471
+ }
472
+ }
473
+ function validateToolArgs(args, schema) {
474
+ if (!schema) {
475
+ return [];
476
+ }
477
+ return validateToolValue(args, schema, "");
478
+ }
479
+ function validateToolValue(value, schema, path) {
480
+ const label = path || "parameter";
481
+ const type = typeof schema.type === "string" ? schema.type : void 0;
482
+ if (type && !matchesSchemaType(value, type)) {
483
+ return [`${label} should be ${type}`];
484
+ }
485
+ const errors = [];
486
+ if (schema.enum && !schema.enum.includes(value)) {
487
+ errors.push(`${label} must be one of ${JSON.stringify(schema.enum)}`);
488
+ }
489
+ if (typeof value === "number") {
490
+ if (schema.minimum !== void 0 && value < schema.minimum) {
491
+ errors.push(`${label} must be >= ${schema.minimum}`);
492
+ }
493
+ if (schema.maximum !== void 0 && value > schema.maximum) {
494
+ errors.push(`${label} must be <= ${schema.maximum}`);
495
+ }
496
+ }
497
+ if (typeof value === "string") {
498
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
499
+ errors.push(`${label} must be at least ${schema.minLength} chars`);
500
+ }
501
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
502
+ errors.push(`${label} must be at most ${schema.maxLength} chars`);
503
+ }
504
+ }
505
+ if (type === "object") {
506
+ const objectValue = value;
507
+ for (const key of schema.required ?? []) {
508
+ if (!(key in objectValue)) {
509
+ errors.push(`missing required ${path ? `${path}.${key}` : key}`);
510
+ }
511
+ }
512
+ const properties = schema.properties ?? {};
513
+ for (const [key, childValue] of Object.entries(objectValue)) {
514
+ const childSchema = properties[key];
515
+ if (!childSchema) {
516
+ continue;
517
+ }
518
+ errors.push(...validateToolValue(childValue, childSchema, path ? `${path}.${key}` : key));
519
+ }
520
+ }
521
+ if (type === "array" && schema.items && Array.isArray(value)) {
522
+ value.forEach((item, index) => {
523
+ errors.push(...validateToolValue(item, schema.items, `${label}[${index}]`));
524
+ });
525
+ }
526
+ return errors;
527
+ }
528
+ function matchesSchemaType(value, type) {
529
+ switch (type) {
530
+ case "string":
531
+ return typeof value === "string";
532
+ case "integer":
533
+ return typeof value === "number" && Number.isInteger(value);
534
+ case "number":
535
+ return typeof value === "number";
536
+ case "boolean":
537
+ return typeof value === "boolean";
538
+ case "array":
539
+ return Array.isArray(value);
540
+ case "object":
541
+ return typeof value === "object" && value !== null && !Array.isArray(value);
542
+ default:
543
+ return true;
544
+ }
545
+ }
546
+ function createInvalidToolArgumentsResult(params) {
547
+ return {
548
+ ok: false,
549
+ error: {
550
+ code: "invalid_tool_arguments",
551
+ message: "Tool arguments are invalid.",
552
+ toolCallId: params.toolCallId,
553
+ toolName: params.toolName,
554
+ rawArgumentsText: params.rawArgumentsText,
555
+ issues: params.issues
556
+ }
557
+ };
558
+ }
559
+ function appendToolRoundToInput(input, reasoning, text, toolResults) {
310
560
  const assistantMsg = {
311
561
  role: "assistant",
312
562
  content: text || null,
563
+ ...reasoning ? { reasoning_content: reasoning } : {},
313
564
  tool_calls: toolResults.map((tr) => ({
314
565
  id: tr.toolCallId,
315
566
  type: "function",
316
567
  function: {
317
568
  name: tr.toolName,
318
- arguments: typeof tr.args === "string" ? tr.args : JSON.stringify(tr.args ?? {})
569
+ arguments: tr.rawArgsText
319
570
  }
320
571
  }))
321
572
  };
@@ -330,6 +581,81 @@ function appendToolRoundToInput(input, text, toolResults) {
330
581
  };
331
582
  }
332
583
 
584
+ // src/round-collector.ts
585
+ import {
586
+ normalizeAssistantText
587
+ } from "@nextclaw/ncp";
588
+ var DefaultNcpRoundCollector = class {
589
+ constructor(reasoningNormalizationMode = "off") {
590
+ this.reasoningNormalizationMode = reasoningNormalizationMode;
591
+ }
592
+ rawText = "";
593
+ explicitReasoning = "";
594
+ toolCallBuffers = /* @__PURE__ */ new Map();
595
+ clear() {
596
+ this.rawText = "";
597
+ this.explicitReasoning = "";
598
+ this.toolCallBuffers.clear();
599
+ }
600
+ consumeChunk(chunk) {
601
+ const choice = chunk.choices?.[0];
602
+ if (!choice) {
603
+ return;
604
+ }
605
+ const delta = choice.delta;
606
+ if (!delta) {
607
+ return;
608
+ }
609
+ if (typeof delta.content === "string" && delta.content.length > 0) {
610
+ this.rawText += delta.content;
611
+ }
612
+ const reasoning = delta.reasoning_content ?? delta.reasoning;
613
+ if (typeof reasoning === "string" && reasoning.length > 0) {
614
+ this.explicitReasoning += reasoning;
615
+ }
616
+ const toolDeltas = delta.tool_calls;
617
+ if (!Array.isArray(toolDeltas)) {
618
+ return;
619
+ }
620
+ for (const toolDelta of toolDeltas) {
621
+ const index = getToolCallIndex(toolDelta, this.toolCallBuffers.size);
622
+ const previous = this.toolCallBuffers.get(index) ?? { argumentsText: "" };
623
+ const current = applyToolDelta(previous, toolDelta);
624
+ this.toolCallBuffers.set(index, current);
625
+ }
626
+ }
627
+ getText() {
628
+ if (this.reasoningNormalizationMode !== "think-tags") {
629
+ return this.rawText;
630
+ }
631
+ return normalizeAssistantText(this.rawText, this.reasoningNormalizationMode).text;
632
+ }
633
+ getReasoning() {
634
+ if (this.explicitReasoning.length > 0) {
635
+ return this.explicitReasoning;
636
+ }
637
+ if (this.reasoningNormalizationMode !== "think-tags") {
638
+ return "";
639
+ }
640
+ return normalizeAssistantText(this.rawText, this.reasoningNormalizationMode).reasoning;
641
+ }
642
+ getToolCalls() {
643
+ const orderedEntries = Array.from(this.toolCallBuffers.entries()).sort(([left], [right]) => left - right);
644
+ const toolCalls = [];
645
+ for (const [, buffer] of orderedEntries) {
646
+ if (!buffer.id || !buffer.name) {
647
+ continue;
648
+ }
649
+ toolCalls.push({
650
+ toolCallId: buffer.id,
651
+ toolName: buffer.name,
652
+ args: buffer.argumentsText
653
+ });
654
+ }
655
+ return toolCalls;
656
+ }
657
+ };
658
+
333
659
  // src/runtime.ts
334
660
  var DefaultNcpAgentRuntime = class {
335
661
  contextBuilder;
@@ -337,12 +663,16 @@ var DefaultNcpAgentRuntime = class {
337
663
  toolRegistry;
338
664
  stateManager;
339
665
  streamEncoder;
666
+ reasoningNormalizationMode;
340
667
  constructor(config) {
341
668
  this.contextBuilder = config.contextBuilder;
342
669
  this.llmApi = config.llmApi;
343
670
  this.toolRegistry = config.toolRegistry;
344
671
  this.stateManager = config.stateManager;
345
- this.streamEncoder = config.streamEncoder ?? new DefaultNcpStreamEncoder();
672
+ this.reasoningNormalizationMode = config.reasoningNormalizationMode ?? "off";
673
+ this.streamEncoder = config.streamEncoder ?? new DefaultNcpStreamEncoder({
674
+ reasoningNormalizationMode: this.reasoningNormalizationMode
675
+ });
346
676
  }
347
677
  run = async function* (input, options) {
348
678
  const ctx = {
@@ -379,55 +709,63 @@ var DefaultNcpAgentRuntime = class {
379
709
  * The stream encoder does not emit RunFinished; it only converts chunks to NCP events.
380
710
  */
381
711
  async *runLoop(llmInput, ctx, options) {
382
- const roundBuffer = new DefaultNcpRoundBuffer();
712
+ const roundCollector = new DefaultNcpRoundCollector(this.reasoningNormalizationMode);
383
713
  let currentInput = llmInput;
384
714
  let done = false;
385
715
  while (!done && !options?.signal?.aborted) {
386
- roundBuffer.clear();
716
+ roundCollector.clear();
387
717
  const stream = this.llmApi.generate(currentInput, { signal: options?.signal });
388
- for await (const event of this.streamEncoder.encode(stream, ctx)) {
718
+ const tappedStream = this.tapStream(stream, (chunk) => roundCollector.consumeChunk(chunk));
719
+ for await (const event of this.streamEncoder.encode(tappedStream, ctx)) {
389
720
  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;
721
+ }
722
+ const toolResults = [];
723
+ for (const toolCall of roundCollector.getToolCalls()) {
724
+ const tool = this.toolRegistry.getTool(toolCall.toolName);
725
+ const parsedArgs = parseToolArgs(toolCall.args);
726
+ let result;
727
+ let args = null;
728
+ if (!parsedArgs.ok) {
729
+ result = createInvalidToolArgumentsResult({
730
+ toolCallId: toolCall.toolCallId,
731
+ toolName: toolCall.toolName,
732
+ rawArgumentsText: parsedArgs.rawText,
733
+ issues: parsedArgs.issues
734
+ });
735
+ } else {
736
+ const schemaIssues = validateToolArgs(parsedArgs.value, tool?.parameters);
737
+ if (schemaIssues.length > 0) {
738
+ result = createInvalidToolArgumentsResult({
739
+ toolCallId: toolCall.toolCallId,
740
+ toolName: toolCall.toolName,
741
+ rawArgumentsText: parsedArgs.rawText,
742
+ issues: schemaIssues
743
+ });
744
+ } else {
745
+ args = parsedArgs.value;
746
+ result = await this.toolRegistry.execute(
747
+ toolCall.toolCallId,
748
+ toolCall.toolName,
749
+ parsedArgs.value
750
+ );
422
751
  }
423
- case NcpEventType3.MessageTextDelta:
424
- roundBuffer.appendText(event.payload.delta);
425
- break;
426
- default:
427
- break;
428
752
  }
753
+ toolResults.push({
754
+ toolCallId: toolCall.toolCallId,
755
+ toolName: toolCall.toolName,
756
+ args,
757
+ rawArgsText: parsedArgs.rawText,
758
+ result
759
+ });
760
+ yield {
761
+ type: NcpEventType3.MessageToolCallResult,
762
+ payload: {
763
+ sessionId: ctx.sessionId,
764
+ toolCallId: toolCall.toolCallId,
765
+ content: result
766
+ }
767
+ };
429
768
  }
430
- const toolResults = roundBuffer.getToolCalls();
431
769
  if (toolResults.length === 0) {
432
770
  yield {
433
771
  type: NcpEventType3.RunFinished,
@@ -438,11 +776,18 @@ var DefaultNcpAgentRuntime = class {
438
776
  }
439
777
  currentInput = appendToolRoundToInput(
440
778
  currentInput,
441
- roundBuffer.getText(),
779
+ roundCollector.getReasoning(),
780
+ roundCollector.getText(),
442
781
  toolResults
443
782
  );
444
783
  }
445
784
  }
785
+ async *tapStream(stream, onChunk) {
786
+ for await (const chunk of stream) {
787
+ onChunk(chunk);
788
+ yield chunk;
789
+ }
790
+ }
446
791
  };
447
792
  export {
448
793
  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.1",
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.1"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.17.6",