@nextclaw/ncp-http-agent-server 0.1.1 → 0.3.0
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/README.md +4 -4
- package/dist/index.d.ts +29 -26
- package/dist/index.js +61 -46
- package/package.json +2 -6
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ HTTP/SSE transport adapter for exposing an `NcpAgentClientEndpoint` over Hono ro
|
|
|
5
5
|
## Build
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pnpm -C packages/nextclaw-ncp-http-agent-server build
|
|
8
|
+
pnpm -C packages/ncp-packages/nextclaw-ncp-http-agent-server build
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## API
|
|
@@ -15,7 +15,7 @@ pnpm -C packages/nextclaw-ncp-http-agent-server build
|
|
|
15
15
|
|
|
16
16
|
**Options:**
|
|
17
17
|
- `agentClientEndpoint` — client endpoint to forward requests to (in-process adapter or remote HTTP client)
|
|
18
|
-
- `
|
|
19
|
-
- `basePath`, `requestTimeoutMs` — path and timeout
|
|
18
|
+
- `streamProvider` (optional) — When set, `/stream` serves stored events instead of forwarding to the agent. Scenario: user reconnects after network drop and wants to continue watching the previous reply; with `streamProvider` we stream from persistence, without it we forward to the agent.
|
|
19
|
+
- `basePath`, `requestTimeoutMs` — path and optional forward-stream timeout. By default no server-side timeout is applied; set a positive value only if you explicitly want the server to cut off long-running forward streams.
|
|
20
20
|
|
|
21
|
-
For in-process agent (`NcpAgentServerEndpoint`), use `createAgentClientFromServer` from `@nextclaw/ncp` to wrap it.
|
|
21
|
+
For in-process agent (`NcpAgentServerEndpoint`), use `createAgentClientFromServer` from `@nextclaw/ncp-toolkit` to wrap it.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NcpStreamRequestPayload, NcpEndpointEvent, NcpAgentClientEndpoint } from '@nextclaw/ncp';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Streams live session events for `/stream`.
|
|
6
6
|
*
|
|
7
7
|
* **Scenario**: User sends a message, agent streams SSE back. Network drops mid-stream.
|
|
8
|
-
* User reconnects and requests `GET /
|
|
9
|
-
*
|
|
8
|
+
* User reconnects and requests `GET /stream?sessionId=xxx` to continue following
|
|
9
|
+
* the current live response for that session.
|
|
10
10
|
*
|
|
11
11
|
* **Two paths**:
|
|
12
|
-
* - **
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - **Forward** (no replayProvider): Forward `message.resume-request` to the agent
|
|
17
|
-
* and let it recover or re-run.
|
|
12
|
+
* - **Session stream** (with streamProvider): Do not call agent again. Attach to the
|
|
13
|
+
* active session's live event stream.
|
|
14
|
+
* - **Forward** (no streamProvider): Forward `message.stream-request` to the agent
|
|
15
|
+
* and let the upstream endpoint provide the live stream.
|
|
18
16
|
*
|
|
19
|
-
* **Implementation**: `stream`
|
|
20
|
-
*
|
|
17
|
+
* **Implementation**: `stream` attaches by payload.sessionId and yields live events
|
|
18
|
+
* for that active session.
|
|
21
19
|
*/
|
|
22
|
-
type
|
|
20
|
+
type NcpHttpAgentStreamProvider = {
|
|
23
21
|
stream(params: {
|
|
24
|
-
payload:
|
|
22
|
+
payload: NcpStreamRequestPayload;
|
|
25
23
|
signal: AbortSignal;
|
|
26
24
|
}): AsyncIterable<NcpEndpointEvent>;
|
|
27
25
|
};
|
|
@@ -29,43 +27,48 @@ type NcpHttpAgentServerOptions = {
|
|
|
29
27
|
/** Client endpoint to forward requests to (in-process adapter or remote HTTP client). */
|
|
30
28
|
agentClientEndpoint: NcpAgentClientEndpoint;
|
|
31
29
|
basePath?: string;
|
|
32
|
-
requestTimeoutMs?: number;
|
|
33
30
|
/**
|
|
34
|
-
* Optional
|
|
35
|
-
* When
|
|
31
|
+
* Optional forward-stream timeout in milliseconds.
|
|
32
|
+
* When omitted or set to a non-positive value, no server-side timeout is applied.
|
|
36
33
|
*/
|
|
37
|
-
|
|
34
|
+
requestTimeoutMs?: number | null;
|
|
35
|
+
/**
|
|
36
|
+
* Optional. When set, `/stream` serves live session events from streamProvider instead of
|
|
37
|
+
* forwarding to the agent.
|
|
38
|
+
* When not set, forwards `message.stream-request` to the agent.
|
|
39
|
+
*/
|
|
40
|
+
streamProvider?: NcpHttpAgentStreamProvider;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
/** Framework-agnostic HTTP handler interface for NCP agent routes. */
|
|
41
44
|
interface NcpHttpAgentHandler {
|
|
42
45
|
handleSend(request: Request): Promise<Response>;
|
|
43
|
-
|
|
46
|
+
handleStream(request: Request): Promise<Response>;
|
|
44
47
|
handleAbort(request: Request): Promise<Response>;
|
|
45
48
|
}
|
|
46
49
|
type NcpHttpAgentHandlerOptions = {
|
|
47
50
|
agentClientEndpoint: NcpAgentClientEndpoint;
|
|
48
51
|
/**
|
|
49
|
-
* Optional. When set, `/
|
|
50
|
-
* See
|
|
52
|
+
* Optional. When set, `/stream` serves stored events instead of forwarding to the agent.
|
|
53
|
+
* See NcpHttpAgentStreamProvider for details.
|
|
51
54
|
*/
|
|
52
|
-
|
|
53
|
-
timeoutMs: number;
|
|
55
|
+
streamProvider?: NcpHttpAgentStreamProvider;
|
|
56
|
+
timeoutMs: number | null;
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
/**
|
|
57
60
|
* Framework-agnostic controller for NCP agent HTTP routes.
|
|
58
|
-
* Forwards /send and /
|
|
61
|
+
* Forwards /send and /stream to agentClientEndpoint; /stream uses streamProvider when set.
|
|
59
62
|
*/
|
|
60
63
|
declare class NcpHttpAgentController implements NcpHttpAgentHandler {
|
|
61
64
|
private readonly options;
|
|
62
65
|
constructor(options: NcpHttpAgentHandlerOptions);
|
|
63
66
|
handleSend(request: Request): Promise<Response>;
|
|
64
|
-
|
|
67
|
+
handleStream(request: Request): Promise<Response>;
|
|
65
68
|
handleAbort(request: Request): Promise<Response>;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
declare function createNcpHttpAgentRouter(options: NcpHttpAgentServerOptions): Hono;
|
|
69
72
|
declare function mountNcpHttpAgentRoutes(app: Hono, options: NcpHttpAgentServerOptions): void;
|
|
70
73
|
|
|
71
|
-
export { NcpHttpAgentController, type NcpHttpAgentHandler, type NcpHttpAgentHandlerOptions, type
|
|
74
|
+
export { NcpHttpAgentController, type NcpHttpAgentHandler, type NcpHttpAgentHandlerOptions, type NcpHttpAgentServerOptions, type NcpHttpAgentStreamProvider, createNcpHttpAgentRouter, mountNcpHttpAgentRoutes };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
// src/controller.ts
|
|
2
|
+
import { NcpEventType } from "@nextclaw/ncp";
|
|
3
|
+
|
|
1
4
|
// src/types.ts
|
|
2
5
|
var DEFAULT_BASE_PATH = "/ncp/agent";
|
|
3
|
-
var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
4
6
|
|
|
5
7
|
// src/parsers.ts
|
|
6
8
|
function normalizeBasePath(basePath) {
|
|
@@ -13,7 +15,10 @@ function normalizeBasePath(basePath) {
|
|
|
13
15
|
}
|
|
14
16
|
function sanitizeTimeout(timeoutMs) {
|
|
15
17
|
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
|
|
16
|
-
return
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (timeoutMs <= 0) {
|
|
21
|
+
return null;
|
|
17
22
|
}
|
|
18
23
|
return Math.max(1e3, Math.trunc(timeoutMs));
|
|
19
24
|
}
|
|
@@ -34,37 +39,33 @@ async function parseRequestEnvelope(request) {
|
|
|
34
39
|
return null;
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
|
-
function
|
|
42
|
+
function parseStreamPayloadFromUrl(url) {
|
|
38
43
|
const query = new URL(url).searchParams;
|
|
39
44
|
const sessionId = query.get("sessionId")?.trim();
|
|
40
|
-
|
|
41
|
-
if (!sessionId || !remoteRunId) {
|
|
45
|
+
if (!sessionId) {
|
|
42
46
|
return null;
|
|
43
47
|
}
|
|
44
|
-
const fromEventIndexRaw = query.get("fromEventIndex");
|
|
45
|
-
const fromEventIndex = typeof fromEventIndexRaw === "string" && Number.isFinite(Number.parseInt(fromEventIndexRaw, 10)) ? Math.max(0, Number.parseInt(fromEventIndexRaw, 10)) : void 0;
|
|
46
48
|
return {
|
|
47
|
-
sessionId
|
|
48
|
-
remoteRunId,
|
|
49
|
-
...typeof fromEventIndex === "number" ? { fromEventIndex } : {}
|
|
49
|
+
sessionId
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
async function parseAbortPayload(request) {
|
|
53
53
|
try {
|
|
54
54
|
const payload = await request.json();
|
|
55
55
|
if (!isRecord(payload)) {
|
|
56
|
-
return
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const sessionId = readTrimmedString(payload.sessionId);
|
|
59
|
+
if (!sessionId) {
|
|
60
|
+
return null;
|
|
57
61
|
}
|
|
58
62
|
const messageId = readTrimmedString(payload.messageId);
|
|
59
|
-
const correlationId = readTrimmedString(payload.correlationId);
|
|
60
|
-
const runId = readTrimmedString(payload.runId);
|
|
61
63
|
return {
|
|
62
|
-
|
|
63
|
-
...
|
|
64
|
-
...runId ? { runId } : {}
|
|
64
|
+
sessionId,
|
|
65
|
+
...messageId ? { messageId } : {}
|
|
65
66
|
};
|
|
66
67
|
} catch {
|
|
67
|
-
return
|
|
68
|
+
return null;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
function readTrimmedString(value) {
|
|
@@ -80,11 +81,10 @@ function extractScopeFromEvent(event) {
|
|
|
80
81
|
if (!payload) {
|
|
81
82
|
return {};
|
|
82
83
|
}
|
|
83
|
-
const runId = readString(payload.runId) ?? readString(payload.remoteRunId);
|
|
84
84
|
return {
|
|
85
85
|
sessionId: readString(payload.sessionId),
|
|
86
86
|
correlationId: readString(payload.correlationId),
|
|
87
|
-
runId
|
|
87
|
+
runId: readString(payload.runId)
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
function matchesScope(scope, event) {
|
|
@@ -275,6 +275,7 @@ async function* createForwardSseEvents(options) {
|
|
|
275
275
|
let timeoutId = null;
|
|
276
276
|
let unsubscribe = null;
|
|
277
277
|
let stopped = false;
|
|
278
|
+
let terminalStopScheduled = false;
|
|
278
279
|
const push = (frame) => {
|
|
279
280
|
if (!stopped) {
|
|
280
281
|
queue.push(frame);
|
|
@@ -297,22 +298,31 @@ async function* createForwardSseEvents(options) {
|
|
|
297
298
|
queue.close();
|
|
298
299
|
};
|
|
299
300
|
requestSignal.addEventListener("abort", stop, { once: true });
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
if (timeoutMs !== null) {
|
|
302
|
+
timeoutId = setTimeout(() => {
|
|
303
|
+
push(toErrorFrame("TIMEOUT", "NCP HTTP stream timed out before terminal event."));
|
|
304
|
+
stop();
|
|
305
|
+
}, timeoutMs);
|
|
306
|
+
}
|
|
304
307
|
unsubscribe = endpoint.subscribe((event) => {
|
|
305
308
|
if (!matchesScope(scope, event)) {
|
|
306
309
|
return;
|
|
307
310
|
}
|
|
308
311
|
push(toNcpEventFrame(event));
|
|
309
312
|
if (isTerminalEvent(event)) {
|
|
310
|
-
|
|
313
|
+
if (!terminalStopScheduled) {
|
|
314
|
+
terminalStopScheduled = true;
|
|
315
|
+
queueMicrotask(stop);
|
|
316
|
+
}
|
|
311
317
|
}
|
|
312
318
|
});
|
|
313
319
|
void endpoint.emit(requestEvent).catch((error) => {
|
|
314
320
|
push(toErrorFrame("EMIT_FAILED", errorMessage(error)));
|
|
315
321
|
stop();
|
|
322
|
+
}).finally(() => {
|
|
323
|
+
if (!terminalStopScheduled) {
|
|
324
|
+
queueMicrotask(stop);
|
|
325
|
+
}
|
|
316
326
|
});
|
|
317
327
|
try {
|
|
318
328
|
for await (const frame of queue.iterable) {
|
|
@@ -322,16 +332,16 @@ async function* createForwardSseEvents(options) {
|
|
|
322
332
|
stop();
|
|
323
333
|
}
|
|
324
334
|
}
|
|
325
|
-
function
|
|
335
|
+
function createLiveStreamResponse(options) {
|
|
326
336
|
const { signal } = options;
|
|
327
337
|
return buildSseResponse(
|
|
328
|
-
createSseEventStream(
|
|
338
|
+
createSseEventStream(createLiveStreamSseEvents(options), signal)
|
|
329
339
|
);
|
|
330
340
|
}
|
|
331
|
-
async function*
|
|
332
|
-
const {
|
|
341
|
+
async function* createLiveStreamSseEvents(options) {
|
|
342
|
+
const { streamProvider, payload, signal } = options;
|
|
333
343
|
try {
|
|
334
|
-
for await (const event of
|
|
344
|
+
for await (const event of streamProvider.stream({ payload, signal })) {
|
|
335
345
|
if (signal.aborted) {
|
|
336
346
|
break;
|
|
337
347
|
}
|
|
@@ -341,7 +351,7 @@ async function* createReplaySseEvents(options) {
|
|
|
341
351
|
}
|
|
342
352
|
}
|
|
343
353
|
} catch (error) {
|
|
344
|
-
yield toErrorFrame("
|
|
354
|
+
yield toErrorFrame("STREAM_SOURCE_FAILED", errorMessage(error));
|
|
345
355
|
}
|
|
346
356
|
}
|
|
347
357
|
|
|
@@ -361,7 +371,7 @@ var NcpHttpAgentController = class {
|
|
|
361
371
|
}
|
|
362
372
|
return createForwardResponse({
|
|
363
373
|
endpoint: agentClientEndpoint,
|
|
364
|
-
requestEvent: { type:
|
|
374
|
+
requestEvent: { type: NcpEventType.MessageRequest, payload: envelope },
|
|
365
375
|
requestSignal: request.signal,
|
|
366
376
|
timeoutMs,
|
|
367
377
|
scope: {
|
|
@@ -370,36 +380,41 @@ var NcpHttpAgentController = class {
|
|
|
370
380
|
}
|
|
371
381
|
});
|
|
372
382
|
}
|
|
373
|
-
async
|
|
374
|
-
const { agentClientEndpoint,
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
383
|
+
async handleStream(request) {
|
|
384
|
+
const { agentClientEndpoint, streamProvider, timeoutMs } = this.options;
|
|
385
|
+
const streamPayload = parseStreamPayloadFromUrl(request.url);
|
|
386
|
+
if (!streamPayload) {
|
|
377
387
|
return jsonResponse(
|
|
378
|
-
{ ok: false, error: { code: "INVALID_QUERY", message: "sessionId
|
|
388
|
+
{ ok: false, error: { code: "INVALID_QUERY", message: "sessionId is required." } },
|
|
379
389
|
400
|
|
380
390
|
);
|
|
381
391
|
}
|
|
382
|
-
if (
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
payload:
|
|
392
|
+
if (streamProvider) {
|
|
393
|
+
return createLiveStreamResponse({
|
|
394
|
+
streamProvider,
|
|
395
|
+
payload: streamPayload,
|
|
386
396
|
signal: request.signal
|
|
387
397
|
});
|
|
388
398
|
}
|
|
389
399
|
return createForwardResponse({
|
|
390
400
|
endpoint: agentClientEndpoint,
|
|
391
|
-
requestEvent: { type:
|
|
401
|
+
requestEvent: { type: NcpEventType.MessageStreamRequest, payload: streamPayload },
|
|
392
402
|
requestSignal: request.signal,
|
|
393
403
|
timeoutMs,
|
|
394
404
|
scope: {
|
|
395
|
-
sessionId:
|
|
396
|
-
runId: resumePayload.remoteRunId
|
|
405
|
+
sessionId: streamPayload.sessionId
|
|
397
406
|
}
|
|
398
407
|
});
|
|
399
408
|
}
|
|
400
409
|
async handleAbort(request) {
|
|
401
410
|
const { agentClientEndpoint } = this.options;
|
|
402
411
|
const payload = await parseAbortPayload(request);
|
|
412
|
+
if (!payload) {
|
|
413
|
+
return jsonResponse(
|
|
414
|
+
{ ok: false, error: { code: "INVALID_BODY", message: "sessionId is required." } },
|
|
415
|
+
400
|
|
416
|
+
);
|
|
417
|
+
}
|
|
403
418
|
await agentClientEndpoint.abort(payload);
|
|
404
419
|
return jsonResponse({ ok: true });
|
|
405
420
|
}
|
|
@@ -413,15 +428,15 @@ function createNcpHttpAgentRouter(options) {
|
|
|
413
428
|
return app;
|
|
414
429
|
}
|
|
415
430
|
function mountNcpHttpAgentRoutes(app, options) {
|
|
416
|
-
const { basePath: rawBasePath, agentClientEndpoint,
|
|
431
|
+
const { basePath: rawBasePath, agentClientEndpoint, streamProvider, requestTimeoutMs } = options;
|
|
417
432
|
const basePath = normalizeBasePath(rawBasePath);
|
|
418
433
|
const controller = new NcpHttpAgentController({
|
|
419
434
|
agentClientEndpoint,
|
|
420
|
-
|
|
435
|
+
streamProvider,
|
|
421
436
|
timeoutMs: sanitizeTimeout(requestTimeoutMs)
|
|
422
437
|
});
|
|
423
438
|
app.post(`${basePath}/send`, (c) => controller.handleSend(c.req.raw));
|
|
424
|
-
app.get(`${basePath}/
|
|
439
|
+
app.get(`${basePath}/stream`, (c) => controller.handleStream(c.req.raw));
|
|
425
440
|
app.post(`${basePath}/abort`, (c) => controller.handleAbort(c.req.raw));
|
|
426
441
|
}
|
|
427
442
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ncp-http-agent-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "HTTP/SSE server transport adapter for NCP agent endpoints.",
|
|
6
6
|
"type": "module",
|
|
@@ -16,14 +16,10 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"hono": "^4.9.8",
|
|
19
|
-
"@nextclaw/ncp": "0.
|
|
19
|
+
"@nextclaw/ncp": "0.3.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.17.6",
|
|
23
|
-
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
24
|
-
"@typescript-eslint/parser": "^7.18.0",
|
|
25
|
-
"eslint": "^8.57.1",
|
|
26
|
-
"eslint-config-prettier": "^9.1.0",
|
|
27
23
|
"prettier": "^3.3.3",
|
|
28
24
|
"tsup": "^8.3.5",
|
|
29
25
|
"typescript": "^5.6.3",
|