@nextclaw/nextclaw-ncp-runtime-stdio-client 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ //#region src/stdio-runtime-error.utils.ts
2
+ function buildSpawnFailureMessage(params) {
3
+ const cwdSuffix = params.cwd ? ` (cwd: ${params.cwd})` : "";
4
+ return `[narp-stdio] failed to start stdio runtime command "${params.command}"${cwdSuffix}: ${params.error.message}`;
5
+ }
6
+ function normalizeRuntimeError(error) {
7
+ if (isNcpError(error)) return error;
8
+ const message = error instanceof Error ? error.message : String(error);
9
+ const lowered = message.toLowerCase();
10
+ return {
11
+ code: lowered.includes("abort") || lowered.includes("cancel") ? "abort-error" : "runtime-error",
12
+ message
13
+ };
14
+ }
15
+ function isAbortLikeRuntimeError(error) {
16
+ if (isNcpError(error)) return error.code === "abort-error";
17
+ const lowered = (error instanceof Error ? error.message : String(error)).toLowerCase();
18
+ return lowered.includes("abort") || lowered.includes("cancel");
19
+ }
20
+ function isNcpError(value) {
21
+ if (!value || typeof value !== "object") return false;
22
+ return typeof value.code === "string" && typeof value.message === "string";
23
+ }
24
+ //#endregion
25
+ export { buildSpawnFailureMessage, isAbortLikeRuntimeError, normalizeRuntimeError };
@@ -0,0 +1,26 @@
1
+ import { readString } from "./stdio-runtime-config.utils.js";
2
+ //#region src/stdio-runtime-tool-name.utils.ts
3
+ function resolveToolNameFromAcpUpdate(update) {
4
+ return resolveToolNameFromRawInput(update.rawInput) ?? resolveHermesAcpToolNameFromTitle(update.title) ?? readString(update.title) ?? "unknown";
5
+ }
6
+ function resolveToolNameFromRawInput(rawInput) {
7
+ if (typeof rawInput !== "object" || !rawInput || Array.isArray(rawInput)) return;
8
+ const input = rawInput;
9
+ return readString(input.toolName) ?? readString(input.tool);
10
+ }
11
+ function resolveHermesAcpToolNameFromTitle(title) {
12
+ const normalizedTitle = readString(title)?.toLowerCase();
13
+ if (!normalizedTitle) return;
14
+ if (normalizedTitle.startsWith("terminal:")) return "terminal";
15
+ if (normalizedTitle.startsWith("read:")) return "read_file";
16
+ if (normalizedTitle.startsWith("write:")) return "write_file";
17
+ if (normalizedTitle.startsWith("patch (") || normalizedTitle.startsWith("patch:")) return "patch";
18
+ if (normalizedTitle.startsWith("search:")) return "search_files";
19
+ if (normalizedTitle.startsWith("web search:")) return "web_search";
20
+ if (normalizedTitle === "web extract" || normalizedTitle.startsWith("extract:")) return "web_extract";
21
+ if (normalizedTitle === "delegate task" || normalizedTitle.startsWith("delegate:")) return "delegate_task";
22
+ if (normalizedTitle === "execute code") return "execute_code";
23
+ if (normalizedTitle.startsWith("analyze image:")) return "vision_analyze";
24
+ }
25
+ //#endregion
26
+ export { resolveToolNameFromAcpUpdate };
@@ -1,10 +1,13 @@
1
1
  import { NARP_STDIO_PROMPT_META_KEY, buildStdioRuntimeLaunchEnv, readString } from "./stdio-runtime-config.utils.js";
2
+ import { buildSpawnFailureMessage, isAbortLikeRuntimeError, normalizeRuntimeError } from "./stdio-runtime-error.utils.js";
3
+ import { resolveToolNameFromAcpUpdate } from "./stdio-runtime-tool-name.utils.js";
2
4
  import { randomUUID } from "node:crypto";
3
5
  import { spawn } from "node:child_process";
4
6
  import { Readable, Writable } from "node:stream";
5
7
  import * as acp from "@agentclientprotocol/sdk";
6
8
  import { NcpEventType } from "@nextclaw/ncp";
7
9
  //#region src/stdio-runtime.service.ts
10
+ const HERMES_ACP_ROUTE_BRIDGE_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE";
8
11
  var UpdateBuffer = class {
9
12
  updates = [];
10
13
  waiters = /* @__PURE__ */ new Set();
@@ -205,7 +208,7 @@ var StdioRuntimeSession = class {
205
208
  });
206
209
  const releaseAbort = this.bindAbortSignal(signal);
207
210
  try {
208
- if (modelId) try {
211
+ if (modelId && this.config.env?.[HERMES_ACP_ROUTE_BRIDGE_ENV] !== "1") try {
209
212
  await this.connection.unstable_setSessionModel({
210
213
  sessionId: this.remoteSessionId,
211
214
  modelId
@@ -247,10 +250,6 @@ var StdioRuntimeSession = class {
247
250
  };
248
251
  readStderr = () => this.stderr;
249
252
  };
250
- function buildSpawnFailureMessage(params) {
251
- const cwdSuffix = params.cwd ? ` (cwd: ${params.cwd})` : "";
252
- return `[narp-stdio] failed to start stdio runtime command "${params.command}"${cwdSuffix}: ${params.error.message}`;
253
- }
254
253
  var StdioRuntimeRunController = class {
255
254
  buffer = new UpdateBuffer();
256
255
  collector = new PromptUpdateCollector();
@@ -287,16 +286,33 @@ var StdioRuntimeRunController = class {
287
286
  signal: options?.signal,
288
287
  onUpdate: (update) => this.buffer.push(update)
289
288
  });
290
- let promptSettled = false;
291
- let promptError = null;
289
+ const promptState = this.trackPromptState(promptPromise);
290
+ yield* this.emitRunStartedEvents(assistantMessageId);
291
+ try {
292
+ yield* this.drainPromptUpdates(assistantMessageId, promptState);
293
+ if (this.shouldExitForAbort(options, promptState.error)) return;
294
+ yield* this.emitCompletionEvents(assistantMessageId);
295
+ } catch (error) {
296
+ if (this.shouldExitForAbort(options, error)) return;
297
+ yield* this.emitFailureEvents(assistantMessageId, error);
298
+ }
299
+ };
300
+ trackPromptState = (promptPromise) => {
301
+ const promptState = {
302
+ settled: false,
303
+ error: null
304
+ };
292
305
  promptPromise.then(() => {
293
- promptSettled = true;
306
+ promptState.settled = true;
294
307
  this.buffer.notify();
295
308
  }).catch((error) => {
296
- promptSettled = true;
297
- promptError = error;
309
+ promptState.settled = true;
310
+ promptState.error = error;
298
311
  this.buffer.notify();
299
312
  });
313
+ return promptState;
314
+ };
315
+ emitRunStartedEvents = async function* (assistantMessageId) {
300
316
  yield* this.emitEvent({
301
317
  type: NcpEventType.MessageAccepted,
302
318
  payload: {
@@ -312,63 +328,65 @@ var StdioRuntimeRunController = class {
312
328
  runId: `narp-stdio:${this.input.sessionId}:${Date.now()}`
313
329
  }
314
330
  });
315
- try {
316
- while (!promptSettled || this.buffer.hasItems()) {
317
- const update = this.buffer.shift();
318
- if (!update) {
319
- await this.buffer.waitForChange();
320
- continue;
321
- }
322
- for (const event of this.translateUpdate(update, assistantMessageId)) yield* this.emitEvent(event);
331
+ };
332
+ drainPromptUpdates = async function* (assistantMessageId, promptState) {
333
+ while (!promptState.settled || this.buffer.hasItems()) {
334
+ const update = this.buffer.shift();
335
+ if (!update) {
336
+ await this.buffer.waitForChange();
337
+ continue;
323
338
  }
324
- if (promptError) throw promptError;
325
- if (options?.signal?.aborted) throw createNcpError("abort-error", "ACP prompt cancelled");
326
- for (const terminalEvent of this.createTerminalEvents(assistantMessageId)) yield* this.emitEvent(terminalEvent);
327
- if (!this.collector.hasParts()) throw new Error(`[narp-stdio] ACP prompt completed without any assistant content for session ${this.input.sessionId}. stderr=${this.session.readStderr()}`);
328
- yield* this.emitEvent({
329
- type: NcpEventType.MessageCompleted,
330
- payload: {
331
- sessionId: this.input.sessionId,
332
- correlationId: this.input.correlationId,
333
- message: {
334
- id: assistantMessageId,
335
- sessionId: this.input.sessionId,
336
- role: "assistant",
337
- status: "final",
338
- parts: this.collector.buildParts(),
339
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
340
- }
341
- }
342
- });
343
- yield* this.emitEvent({
344
- type: NcpEventType.RunFinished,
345
- payload: {
346
- sessionId: this.input.sessionId,
347
- messageId: assistantMessageId,
348
- runId: `narp-stdio:${this.input.sessionId}`
349
- }
350
- });
351
- } catch (error) {
352
- const ncpError = normalizeRuntimeError(error);
353
- yield* this.emitEvent({
354
- type: NcpEventType.MessageFailed,
355
- payload: {
356
- sessionId: this.input.sessionId,
357
- messageId: assistantMessageId,
358
- correlationId: this.input.correlationId,
359
- error: ncpError
360
- }
361
- });
362
- yield* this.emitEvent({
363
- type: NcpEventType.RunError,
364
- payload: {
339
+ for (const event of this.translateUpdate(update, assistantMessageId)) yield* this.emitEvent(event);
340
+ }
341
+ };
342
+ shouldExitForAbort = (options, error) => options?.signal?.aborted === true || isAbortLikeRuntimeError(error);
343
+ emitCompletionEvents = async function* (assistantMessageId) {
344
+ for (const terminalEvent of this.createTerminalEvents(assistantMessageId)) yield* this.emitEvent(terminalEvent);
345
+ if (!this.collector.hasParts()) throw new Error(`[narp-stdio] ACP prompt completed without any assistant content for session ${this.input.sessionId}. stderr=${this.session.readStderr()}`);
346
+ yield* this.emitEvent({
347
+ type: NcpEventType.MessageCompleted,
348
+ payload: {
349
+ sessionId: this.input.sessionId,
350
+ correlationId: this.input.correlationId,
351
+ message: {
352
+ id: assistantMessageId,
365
353
  sessionId: this.input.sessionId,
366
- messageId: assistantMessageId,
367
- runId: `narp-stdio:${this.input.sessionId}`,
368
- error: ncpError.message
354
+ role: "assistant",
355
+ status: "final",
356
+ parts: this.collector.buildParts(),
357
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
369
358
  }
370
- });
371
- }
359
+ }
360
+ });
361
+ yield* this.emitEvent({
362
+ type: NcpEventType.RunFinished,
363
+ payload: {
364
+ sessionId: this.input.sessionId,
365
+ messageId: assistantMessageId,
366
+ runId: `narp-stdio:${this.input.sessionId}`
367
+ }
368
+ });
369
+ };
370
+ emitFailureEvents = async function* (assistantMessageId, error) {
371
+ const ncpError = normalizeRuntimeError(error);
372
+ yield* this.emitEvent({
373
+ type: NcpEventType.MessageFailed,
374
+ payload: {
375
+ sessionId: this.input.sessionId,
376
+ messageId: assistantMessageId,
377
+ correlationId: this.input.correlationId,
378
+ error: ncpError
379
+ }
380
+ });
381
+ yield* this.emitEvent({
382
+ type: NcpEventType.RunError,
383
+ payload: {
384
+ sessionId: this.input.sessionId,
385
+ messageId: assistantMessageId,
386
+ runId: `narp-stdio:${this.input.sessionId}`,
387
+ error: ncpError.message
388
+ }
389
+ });
372
390
  };
373
391
  emitEvent = async function* (event) {
374
392
  this.collector.apply(event);
@@ -553,11 +571,7 @@ function resolveModelId(params) {
553
571
  return providerRoute?.model ?? readString(metadata?.preferred_model) ?? readString(metadata?.preferredModel) ?? readString(metadata?.model);
554
572
  }
555
573
  function resolveToolName(update) {
556
- if (typeof update.rawInput === "object" && update.rawInput && !Array.isArray(update.rawInput)) {
557
- const candidate = readString(update.rawInput.toolName) ?? readString(update.rawInput.tool);
558
- if (candidate) return candidate;
559
- }
560
- return readString(update.title) ?? "unknown";
574
+ return resolveToolNameFromAcpUpdate(update);
561
575
  }
562
576
  function createAssistantMessageId(requestMessageId) {
563
577
  return `assistant:${requestMessageId.trim() || "request"}:${randomUUID()}`;
@@ -567,25 +581,6 @@ function serializeToolArgs(value) {
567
581
  if (typeof value === "undefined") return "{}";
568
582
  return JSON.stringify(value);
569
583
  }
570
- function normalizeRuntimeError(error) {
571
- if (isNcpError(error)) return error;
572
- const message = error instanceof Error ? error.message : String(error);
573
- const lowered = message.toLowerCase();
574
- return {
575
- code: lowered.includes("abort") || lowered.includes("cancel") ? "abort-error" : "runtime-error",
576
- message
577
- };
578
- }
579
- function createNcpError(code, message) {
580
- return {
581
- code,
582
- message
583
- };
584
- }
585
- function isNcpError(value) {
586
- if (!value || typeof value !== "object") return false;
587
- return typeof value.code === "string" && typeof value.message === "string";
588
- }
589
584
  async function withTimeout(promise, timeoutMs, message) {
590
585
  let timeoutHandle = null;
591
586
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/nextclaw-ncp-runtime-stdio-client",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "Optional NCP runtime adapter backed by a protocol-aware stdio agent command.",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@agentclientprotocol/sdk": "^0.19.0",
20
- "@nextclaw/ncp": "0.5.2"
20
+ "@nextclaw/ncp": "0.5.4"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^20.17.6",