@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 +2 -2
- package/dist/index.js +2 -2
- package/dist/stdio-runtime-config.utils.d.ts +6 -1
- package/dist/stdio-runtime-config.utils.js +10 -1
- package/dist/stdio-runtime-error.utils.js +25 -0
- package/dist/stdio-runtime-probe.utils.js +2 -5
- package/dist/stdio-runtime-tool-name.utils.js +26 -0
- package/dist/stdio-runtime.service.js +86 -92
- package/package.json +3 -3
- package/dist/hermes-acp-route-bridge/__pycache__/sitecustomize.cpython-312.pyc +0 -0
- package/dist/hermes-acp-route-bridge/__pycache__/sitecustomize.cpython-314.pyc +0 -0
- package/dist/hermes-acp-route-bridge/sitecustomize.py +0 -225
- package/dist/hermes-acp-route-bridge.utils.js +0 -61
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 {
|
|
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:
|
|
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 {
|
|
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 =
|
|
160
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
306
|
+
promptState.settled = true;
|
|
295
307
|
this.buffer.notify();
|
|
296
308
|
}).catch((error) => {
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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"
|
|
Binary file
|
|
Binary file
|
|
@@ -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 };
|