@nextclaw/nextclaw-ncp-runtime-stdio-client 0.1.1 → 0.1.3

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 { NARP_STDIO_PROMPT_META_KEY, NarpStdioPromptMeta, StdioRuntimeConfigResolver, StdioRuntimeEnv, StdioRuntimeProcessScope, StdioRuntimeResolvedConfig, StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv } from "./stdio-runtime-config.utils.js";
1
+ import { NARP_STDIO_PROMPT_META_KEY, NarpStdioPromptMeta, StdioRuntimeConfigResolver, StdioRuntimeEnv, StdioRuntimeProcessScope, StdioRuntimeResolvedConfig, StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv } from "./stdio-runtime-config.utils.js";
2
2
  import { StdioRuntimeNcpAgentRuntime, StdioRuntimeNcpAgentRuntimeConfig } from "./stdio-runtime.service.js";
3
3
  import { probeStdioRuntime } from "./stdio-runtime-probe.utils.js";
4
- export { NARP_STDIO_PROMPT_META_KEY, type NarpStdioPromptMeta, StdioRuntimeConfigResolver, type StdioRuntimeEnv, StdioRuntimeNcpAgentRuntime, type StdioRuntimeNcpAgentRuntimeConfig, type StdioRuntimeProcessScope, type StdioRuntimeResolvedConfig, type StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv, probeStdioRuntime };
4
+ export { NARP_STDIO_PROMPT_META_KEY, type NarpStdioPromptMeta, StdioRuntimeConfigResolver, type StdioRuntimeEnv, StdioRuntimeNcpAgentRuntime, type StdioRuntimeNcpAgentRuntimeConfig, type StdioRuntimeProcessScope, type StdioRuntimeResolvedConfig, type StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv, probeStdioRuntime };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, buildRuntimeRouteBridgeEnv } from "./stdio-runtime-config.utils.js";
1
+ import { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv } from "./stdio-runtime-config.utils.js";
2
2
  import { StdioRuntimeNcpAgentRuntime } from "./stdio-runtime.service.js";
3
3
  import { probeStdioRuntime } from "./stdio-runtime-probe.utils.js";
4
- export { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, StdioRuntimeNcpAgentRuntime, buildRuntimeRouteBridgeEnv, probeStdioRuntime };
4
+ export { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, StdioRuntimeNcpAgentRuntime, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv, probeStdioRuntime };
@@ -32,5 +32,10 @@ declare class StdioRuntimeConfigResolver {
32
32
  declare function buildRuntimeRouteBridgeEnv(params: {
33
33
  providerRoute?: NcpProviderRuntimeRoute;
34
34
  }): Record<string, string>;
35
+ declare function buildStdioRuntimeLaunchEnv(params: {
36
+ baseEnv?: Record<string, string | undefined>;
37
+ configEnv?: StdioRuntimeEnv;
38
+ providerRoute?: NcpProviderRuntimeRoute;
39
+ }): Record<string, string>;
35
40
  //#endregion
36
- export { NARP_STDIO_PROMPT_META_KEY, NarpStdioPromptMeta, StdioRuntimeConfigResolver, StdioRuntimeEnv, StdioRuntimeProcessScope, StdioRuntimeResolvedConfig, StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv };
41
+ export { NARP_STDIO_PROMPT_META_KEY, NarpStdioPromptMeta, StdioRuntimeConfigResolver, StdioRuntimeEnv, StdioRuntimeProcessScope, StdioRuntimeResolvedConfig, StdioRuntimeWireDialect, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv };
@@ -82,6 +82,15 @@ function buildRuntimeRouteBridgeEnv(params) {
82
82
  [DEFAULT_RUNTIME_ROUTE_BRIDGE_FIELDS.headers]: JSON.stringify(providerRoute.headers ?? {})
83
83
  };
84
84
  }
85
+ function buildStdioRuntimeLaunchEnv(params) {
86
+ const baseEnv = {};
87
+ for (const [key, value] of Object.entries(params.baseEnv ?? process.env)) if (typeof value === "string") baseEnv[key] = value;
88
+ return {
89
+ ...baseEnv,
90
+ ...params.configEnv ?? {},
91
+ ...buildRuntimeRouteBridgeEnv({ providerRoute: params.providerRoute })
92
+ };
93
+ }
85
94
  function parseJsonArray(value) {
86
95
  if (typeof value !== "string" || !value.trim()) return;
87
96
  try {
@@ -99,4 +108,4 @@ function parseJsonObject(value) {
99
108
  }
100
109
  }
101
110
  //#endregion
102
- export { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, buildRuntimeRouteBridgeEnv, readString };
111
+ export { NARP_STDIO_PROMPT_META_KEY, StdioRuntimeConfigResolver, buildRuntimeRouteBridgeEnv, buildStdioRuntimeLaunchEnv, readString };
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { buildStdioLaunchEnv } from "./hermes-acp-route-bridge.utils.js";
1
+ import { buildStdioRuntimeLaunchEnv } from "./stdio-runtime-config.utils.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { Readable, Writable } from "node:stream";
4
4
  import * as acp from "@agentclientprotocol/sdk";
@@ -12,10 +12,7 @@ const createProbeClientBridge = () => ({
12
12
  async function probeStdioRuntime(config) {
13
13
  const child = spawn(config.command, config.args, {
14
14
  cwd: config.cwd,
15
- env: buildStdioLaunchEnv({
16
- config,
17
- useProbeRoute: true
18
- }),
15
+ env: buildStdioRuntimeLaunchEnv({ configEnv: config.env }),
19
16
  stdio: [
20
17
  "pipe",
21
18
  "pipe",
@@ -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,11 +1,13 @@
1
- import { NARP_STDIO_PROMPT_META_KEY, readString } from "./stdio-runtime-config.utils.js";
2
- import { buildStdioLaunchEnv } from "./hermes-acp-route-bridge.utils.js";
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";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import { spawn } from "node:child_process";
5
6
  import { Readable, Writable } from "node:stream";
6
7
  import * as acp from "@agentclientprotocol/sdk";
7
8
  import { NcpEventType } from "@nextclaw/ncp";
8
9
  //#region src/stdio-runtime.service.ts
10
+ const HERMES_ACP_ROUTE_BRIDGE_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE";
9
11
  var UpdateBuffer = class {
10
12
  updates = [];
11
13
  waiters = /* @__PURE__ */ new Set();
@@ -156,8 +158,8 @@ var StdioRuntimeSession = class {
156
158
  ensureStarted = async (params) => {
157
159
  this.pendingProviderRoute = params?.providerRoute;
158
160
  if (this.connection && this.remoteSessionId) return;
159
- const env = buildStdioLaunchEnv({
160
- config: this.config,
161
+ const env = buildStdioRuntimeLaunchEnv({
162
+ configEnv: this.config.env,
161
163
  providerRoute: this.pendingProviderRoute
162
164
  });
163
165
  this.child = spawn(this.config.command, this.config.args, {
@@ -206,7 +208,7 @@ var StdioRuntimeSession = class {
206
208
  });
207
209
  const releaseAbort = this.bindAbortSignal(signal);
208
210
  try {
209
- if (modelId) try {
211
+ if (modelId && this.config.env?.[HERMES_ACP_ROUTE_BRIDGE_ENV] !== "1") try {
210
212
  await this.connection.unstable_setSessionModel({
211
213
  sessionId: this.remoteSessionId,
212
214
  modelId
@@ -248,10 +250,6 @@ var StdioRuntimeSession = class {
248
250
  };
249
251
  readStderr = () => this.stderr;
250
252
  };
251
- function buildSpawnFailureMessage(params) {
252
- const cwdSuffix = params.cwd ? ` (cwd: ${params.cwd})` : "";
253
- return `[narp-stdio] failed to start stdio runtime command "${params.command}"${cwdSuffix}: ${params.error.message}`;
254
- }
255
253
  var StdioRuntimeRunController = class {
256
254
  buffer = new UpdateBuffer();
257
255
  collector = new PromptUpdateCollector();
@@ -288,16 +286,33 @@ var StdioRuntimeRunController = class {
288
286
  signal: options?.signal,
289
287
  onUpdate: (update) => this.buffer.push(update)
290
288
  });
291
- let promptSettled = false;
292
- 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
+ };
293
305
  promptPromise.then(() => {
294
- promptSettled = true;
306
+ promptState.settled = true;
295
307
  this.buffer.notify();
296
308
  }).catch((error) => {
297
- promptSettled = true;
298
- promptError = error;
309
+ promptState.settled = true;
310
+ promptState.error = error;
299
311
  this.buffer.notify();
300
312
  });
313
+ return promptState;
314
+ };
315
+ emitRunStartedEvents = async function* (assistantMessageId) {
301
316
  yield* this.emitEvent({
302
317
  type: NcpEventType.MessageAccepted,
303
318
  payload: {
@@ -313,63 +328,65 @@ var StdioRuntimeRunController = class {
313
328
  runId: `narp-stdio:${this.input.sessionId}:${Date.now()}`
314
329
  }
315
330
  });
316
- try {
317
- while (!promptSettled || this.buffer.hasItems()) {
318
- const update = this.buffer.shift();
319
- if (!update) {
320
- await this.buffer.waitForChange();
321
- continue;
322
- }
323
- 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;
324
338
  }
325
- if (promptError) throw promptError;
326
- if (options?.signal?.aborted) throw createNcpError("abort-error", "ACP prompt cancelled");
327
- for (const terminalEvent of this.createTerminalEvents(assistantMessageId)) yield* this.emitEvent(terminalEvent);
328
- 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()}`);
329
- yield* this.emitEvent({
330
- type: NcpEventType.MessageCompleted,
331
- payload: {
332
- sessionId: this.input.sessionId,
333
- correlationId: this.input.correlationId,
334
- message: {
335
- id: assistantMessageId,
336
- sessionId: this.input.sessionId,
337
- role: "assistant",
338
- status: "final",
339
- parts: this.collector.buildParts(),
340
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
341
- }
342
- }
343
- });
344
- yield* this.emitEvent({
345
- type: NcpEventType.RunFinished,
346
- payload: {
347
- sessionId: this.input.sessionId,
348
- messageId: assistantMessageId,
349
- runId: `narp-stdio:${this.input.sessionId}`
350
- }
351
- });
352
- } catch (error) {
353
- const ncpError = normalizeRuntimeError(error);
354
- yield* this.emitEvent({
355
- type: NcpEventType.MessageFailed,
356
- payload: {
357
- sessionId: this.input.sessionId,
358
- messageId: assistantMessageId,
359
- correlationId: this.input.correlationId,
360
- error: ncpError
361
- }
362
- });
363
- yield* this.emitEvent({
364
- type: NcpEventType.RunError,
365
- 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,
366
353
  sessionId: this.input.sessionId,
367
- messageId: assistantMessageId,
368
- runId: `narp-stdio:${this.input.sessionId}`,
369
- error: ncpError.message
354
+ role: "assistant",
355
+ status: "final",
356
+ parts: this.collector.buildParts(),
357
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
370
358
  }
371
- });
372
- }
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
+ });
373
390
  };
374
391
  emitEvent = async function* (event) {
375
392
  this.collector.apply(event);
@@ -554,11 +571,7 @@ function resolveModelId(params) {
554
571
  return providerRoute?.model ?? readString(metadata?.preferred_model) ?? readString(metadata?.preferredModel) ?? readString(metadata?.model);
555
572
  }
556
573
  function resolveToolName(update) {
557
- if (typeof update.rawInput === "object" && update.rawInput && !Array.isArray(update.rawInput)) {
558
- const candidate = readString(update.rawInput.toolName) ?? readString(update.rawInput.tool);
559
- if (candidate) return candidate;
560
- }
561
- return readString(update.title) ?? "unknown";
574
+ return resolveToolNameFromAcpUpdate(update);
562
575
  }
563
576
  function createAssistantMessageId(requestMessageId) {
564
577
  return `assistant:${requestMessageId.trim() || "request"}:${randomUUID()}`;
@@ -568,25 +581,6 @@ function serializeToolArgs(value) {
568
581
  if (typeof value === "undefined") return "{}";
569
582
  return JSON.stringify(value);
570
583
  }
571
- function normalizeRuntimeError(error) {
572
- if (isNcpError(error)) return error;
573
- const message = error instanceof Error ? error.message : String(error);
574
- const lowered = message.toLowerCase();
575
- return {
576
- code: lowered.includes("abort") || lowered.includes("cancel") ? "abort-error" : "runtime-error",
577
- message
578
- };
579
- }
580
- function createNcpError(code, message) {
581
- return {
582
- code,
583
- message
584
- };
585
- }
586
- function isNcpError(value) {
587
- if (!value || typeof value !== "object") return false;
588
- return typeof value.code === "string" && typeof value.message === "string";
589
- }
590
584
  async function withTimeout(promise, timeoutMs, message) {
591
585
  let timeoutHandle = null;
592
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.1",
3
+ "version": "0.1.3",
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.3"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^20.17.6",
@@ -25,7 +25,7 @@
25
25
  "vitest": "^4.1.2"
26
26
  },
27
27
  "scripts": {
28
- "build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle && node ./scripts/copy-hermes-acp-route-bridge.mjs",
28
+ "build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle",
29
29
  "lint": "eslint .",
30
30
  "tsc": "tsc -p tsconfig.json",
31
31
  "test": "vitest run"
@@ -1,225 +0,0 @@
1
- """NextClaw bridge for Hermes ACP RuntimeRoute passthrough.
2
-
3
- This module is auto-imported by Python when PYTHONPATH points at this
4
- directory. It patches Hermes ACP so `hermes acp` consumes the active
5
- NextClaw RuntimeRoute instead of resolving its own provider config first.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- import logging
12
- import os
13
- from typing import Any, Dict, Optional
14
-
15
- LOGGER = logging.getLogger("nextclaw.hermes_acp_route_bridge")
16
- ROUTE_ENABLE_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE"
17
- API_MODE_HEADER = "x-nextclaw-narp-api-mode"
18
-
19
-
20
- def _read_text_env(name: str) -> Optional[str]:
21
- value = os.environ.get(name)
22
- if not isinstance(value, str):
23
- return None
24
- trimmed = value.strip()
25
- return trimmed or None
26
-
27
-
28
- def _read_headers_env() -> Dict[str, str]:
29
- raw = _read_text_env("NEXTCLAW_HEADERS_JSON")
30
- if not raw:
31
- return {}
32
- try:
33
- parsed = json.loads(raw)
34
- except Exception:
35
- LOGGER.debug("Failed to parse NEXTCLAW_HEADERS_JSON", exc_info=True)
36
- return {}
37
- if not isinstance(parsed, dict):
38
- return {}
39
- normalized: Dict[str, str] = {}
40
- for key, value in parsed.items():
41
- if isinstance(key, str) and isinstance(value, str) and key.strip() and value.strip():
42
- normalized[key] = value
43
- return normalized
44
-
45
-
46
- def _read_nextclaw_route(explicit_model: Optional[str] = None) -> Optional[Dict[str, Any]]:
47
- model = explicit_model or _read_text_env("NEXTCLAW_MODEL")
48
- api_base = _read_text_env("NEXTCLAW_API_BASE")
49
- api_key = _read_text_env("NEXTCLAW_API_KEY")
50
- headers = _read_headers_env()
51
- api_mode = headers.pop(API_MODE_HEADER, None) or "chat_completions"
52
- if not any([model, api_base, api_key, headers]):
53
- return None
54
- return {
55
- "model": model,
56
- "api_base": api_base,
57
- "api_key": api_key or "",
58
- "headers": headers,
59
- "api_mode": api_mode,
60
- }
61
-
62
-
63
- def _merge_openai_headers(agent: Any, headers: Dict[str, str]) -> None:
64
- if not headers:
65
- return
66
- client_kwargs = getattr(agent, "_client_kwargs", None)
67
- if not isinstance(client_kwargs, dict):
68
- return
69
- merged = dict(client_kwargs.get("default_headers") or {})
70
- merged.update(headers)
71
- client_kwargs["default_headers"] = merged
72
- replace_client = getattr(agent, "_replace_primary_openai_client", None)
73
- if callable(replace_client):
74
- replace_client(reason="nextclaw_runtime_route_bridge")
75
-
76
-
77
- def _merge_anthropic_headers(agent: Any, headers: Dict[str, str]) -> None:
78
- if not headers:
79
- return
80
- client = getattr(agent, "_anthropic_client", None)
81
- if client is None:
82
- return
83
- merged = dict(getattr(client, "_default_headers", {}) or {})
84
- merged.update(headers)
85
- if hasattr(client, "_default_headers"):
86
- client._default_headers = merged
87
- inner_client = getattr(client, "_client", None)
88
- if inner_client is not None:
89
- if hasattr(inner_client, "_default_headers"):
90
- inner_client._default_headers = merged
91
- transport_headers = getattr(inner_client, "headers", None)
92
- if hasattr(transport_headers, "update"):
93
- transport_headers.update(merged)
94
-
95
-
96
- def _patch_acp_auth() -> None:
97
- try:
98
- from acp_adapter import auth as auth_module
99
- except Exception:
100
- LOGGER.debug("Failed to import acp_adapter.auth", exc_info=True)
101
- return
102
-
103
- original_detect_provider = auth_module.detect_provider
104
-
105
- def detect_provider():
106
- route = _read_nextclaw_route()
107
- if route is not None:
108
- return "nextclaw"
109
- return original_detect_provider()
110
-
111
- def has_provider():
112
- return detect_provider() is not None
113
-
114
- auth_module.detect_provider = detect_provider
115
- auth_module.has_provider = has_provider
116
-
117
-
118
- def _patch_session_manager() -> None:
119
- try:
120
- from acp_adapter import session as session_module
121
- except Exception:
122
- LOGGER.debug("Failed to import acp_adapter.session", exc_info=True)
123
- return
124
-
125
- original_make_agent = session_module.SessionManager._make_agent
126
-
127
- def bridged_make_agent(
128
- self,
129
- *,
130
- session_id: str,
131
- cwd: str,
132
- model: str | None = None,
133
- requested_provider: str | None = None,
134
- base_url: str | None = None,
135
- api_mode: str | None = None,
136
- ):
137
- route = _read_nextclaw_route(explicit_model=model)
138
- if route is None:
139
- return original_make_agent(
140
- self,
141
- session_id=session_id,
142
- cwd=cwd,
143
- model=model,
144
- requested_provider=requested_provider,
145
- base_url=base_url,
146
- api_mode=api_mode,
147
- )
148
-
149
- if self._agent_factory is not None:
150
- return self._agent_factory()
151
-
152
- from run_agent import AIAgent
153
-
154
- kwargs = {
155
- "platform": "acp",
156
- "enabled_toolsets": ["hermes-acp"],
157
- "quiet_mode": True,
158
- "session_id": session_id,
159
- "model": route["model"] or "",
160
- "provider": requested_provider or "custom",
161
- "api_mode": api_mode or route["api_mode"],
162
- "base_url": base_url or route["api_base"] or "",
163
- "api_key": route["api_key"],
164
- }
165
-
166
- session_module._register_task_cwd(session_id, cwd)
167
- agent = AIAgent(**kwargs)
168
- agent._print_fn = session_module._acp_stderr_print
169
- _merge_openai_headers(agent, route["headers"])
170
- _merge_anthropic_headers(agent, route["headers"])
171
- return agent
172
-
173
- session_module.SessionManager._make_agent = bridged_make_agent
174
-
175
-
176
- def _patch_acp_reasoning_mapping() -> None:
177
- try:
178
- from run_agent import AIAgent
179
- except Exception:
180
- LOGGER.debug("Failed to import run_agent.AIAgent", exc_info=True)
181
- return
182
-
183
- original_run_conversation = getattr(AIAgent, "run_conversation", None)
184
- if not callable(original_run_conversation):
185
- return
186
- if getattr(original_run_conversation, "__nextclaw_reasoning_bridge__", False):
187
- return
188
-
189
- def bridged_run_conversation(self, *args, **kwargs):
190
- original_thinking = getattr(self, "thinking_callback", None)
191
- original_reasoning = getattr(self, "reasoning_callback", None)
192
- remapped = (
193
- getattr(self, "platform", None) == "acp"
194
- and callable(original_thinking)
195
- and original_reasoning is None
196
- )
197
-
198
- if remapped:
199
- # Hermes ACP currently wires its transient spinner/status callback
200
- # into ACP thought events. For ACP sessions, remap that callback to
201
- # the real reasoning channel so downstream clients receive model
202
- # reasoning instead of strings like "(⌐■_■) computing...".
203
- self.reasoning_callback = original_thinking
204
- self.thinking_callback = None
205
-
206
- try:
207
- return original_run_conversation(self, *args, **kwargs)
208
- finally:
209
- if remapped:
210
- self.thinking_callback = original_thinking
211
- self.reasoning_callback = original_reasoning
212
-
213
- bridged_run_conversation.__nextclaw_reasoning_bridge__ = True
214
- AIAgent.run_conversation = bridged_run_conversation
215
-
216
-
217
- def _activate() -> None:
218
- if _read_text_env(ROUTE_ENABLE_ENV) != "1":
219
- return
220
- _patch_acp_auth()
221
- _patch_session_manager()
222
- _patch_acp_reasoning_mapping()
223
-
224
-
225
- _activate()
@@ -1,61 +0,0 @@
1
- import { buildRuntimeRouteBridgeEnv } from "./stdio-runtime-config.utils.js";
2
- import { existsSync } from "node:fs";
3
- import { basename, delimiter } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- //#region src/hermes-acp-route-bridge.utils.ts
6
- const HERMES_ACP_BRIDGE_ENABLE_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE";
7
- const HERMES_ACP_BRIDGE_DIR_ENV = "NEXTCLAW_HERMES_ACP_ROUTE_BRIDGE_DIR";
8
- const HERMES_ACP_PROBE_ROUTE = {
9
- model: "nextclaw-hermes-acp-probe",
10
- apiBase: "http://127.0.0.1:1/v1",
11
- apiKey: "nextclaw-hermes-acp-probe-key",
12
- headers: { "x-nextclaw-narp-api-mode": "chat_completions" }
13
- };
14
- function normalizeCommandBasename(command) {
15
- return basename(command).trim().toLowerCase();
16
- }
17
- function isPythonCommand(command) {
18
- const normalized = normalizeCommandBasename(command);
19
- return normalized === "python" || normalized === "python3" || normalized.startsWith("python3.");
20
- }
21
- function targetsHermesAcpModule(args) {
22
- for (let index = 0; index < args.length - 1; index += 1) if (args[index] === "-m" && args[index + 1]?.trim().toLowerCase() === "acp_adapter.entry") return true;
23
- return false;
24
- }
25
- function isHermesAcpRuntimeConfig(config) {
26
- const command = normalizeCommandBasename(config.command);
27
- if (command === "hermes-acp") return true;
28
- if (command === "hermes") return config.args[0]?.trim().toLowerCase() === "acp";
29
- if (isPythonCommand(config.command)) return targetsHermesAcpModule(config.args);
30
- return false;
31
- }
32
- function resolveHermesAcpBridgeDir() {
33
- const bridgeDir = fileURLToPath(new URL("./hermes-acp-route-bridge", import.meta.url));
34
- return existsSync(bridgeDir) ? bridgeDir : null;
35
- }
36
- function mergePathList(existingValue, injectedPath) {
37
- const parts = (existingValue ?? "").split(delimiter).map((entry) => entry.trim()).filter(Boolean);
38
- if (!parts.includes(injectedPath)) parts.unshift(injectedPath);
39
- return parts.join(delimiter);
40
- }
41
- function buildStdioLaunchEnv(params) {
42
- const providerRoute = params.providerRoute ?? (params.useProbeRoute && isHermesAcpRuntimeConfig(params.config) ? HERMES_ACP_PROBE_ROUTE : void 0);
43
- const baseEnv = {};
44
- for (const [key, value] of Object.entries(params.baseEnv ?? process.env)) if (typeof value === "string") baseEnv[key] = value;
45
- const env = {
46
- ...baseEnv,
47
- ...params.config.env ?? {},
48
- ...buildRuntimeRouteBridgeEnv({ providerRoute })
49
- };
50
- if (!isHermesAcpRuntimeConfig(params.config)) return env;
51
- const bridgeDir = resolveHermesAcpBridgeDir();
52
- if (!bridgeDir) return env;
53
- return {
54
- ...env,
55
- [HERMES_ACP_BRIDGE_ENABLE_ENV]: "1",
56
- [HERMES_ACP_BRIDGE_DIR_ENV]: bridgeDir,
57
- PYTHONPATH: mergePathList(env.PYTHONPATH, bridgeDir)
58
- };
59
- }
60
- //#endregion
61
- export { buildStdioLaunchEnv };