@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 +10 -2
- package/dist/index.js +406 -61
- package/package.json +2 -2
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
|
-
|
|
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({
|
|
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
|
|
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:
|
|
76
|
-
description:
|
|
77
|
-
parameters:
|
|
120
|
+
name: definition.name,
|
|
121
|
+
description: definition.description,
|
|
122
|
+
parameters: definition.parameters
|
|
78
123
|
}
|
|
79
|
-
}))
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
420
|
+
return JSON.stringify(args);
|
|
303
421
|
} catch {
|
|
304
|
-
return
|
|
422
|
+
return "[unserializable-object]";
|
|
305
423
|
}
|
|
306
424
|
}
|
|
307
|
-
return args;
|
|
425
|
+
return String(args ?? "");
|
|
308
426
|
}
|
|
309
|
-
function
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
716
|
+
roundCollector.clear();
|
|
387
717
|
const stream = this.llmApi.generate(currentInput, { signal: options?.signal });
|
|
388
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
18
|
+
"@nextclaw/ncp": "0.3.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.17.6",
|