@nextclaw/ncp-http-agent-server 0.1.1 → 0.2.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 +3 -3
- package/dist/index.d.ts +23 -24
- package/dist/index.js +51 -40
- 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
|
-
- `
|
|
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
19
|
- `basePath`, `requestTimeoutMs` — path and timeout
|
|
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
|
};
|
|
@@ -31,41 +29,42 @@ type NcpHttpAgentServerOptions = {
|
|
|
31
29
|
basePath?: string;
|
|
32
30
|
requestTimeoutMs?: number;
|
|
33
31
|
/**
|
|
34
|
-
* Optional. When set, `/
|
|
35
|
-
*
|
|
32
|
+
* Optional. When set, `/stream` serves live session events from streamProvider instead of
|
|
33
|
+
* forwarding to the agent.
|
|
34
|
+
* When not set, forwards `message.stream-request` to the agent.
|
|
36
35
|
*/
|
|
37
|
-
|
|
36
|
+
streamProvider?: NcpHttpAgentStreamProvider;
|
|
38
37
|
};
|
|
39
38
|
|
|
40
39
|
/** Framework-agnostic HTTP handler interface for NCP agent routes. */
|
|
41
40
|
interface NcpHttpAgentHandler {
|
|
42
41
|
handleSend(request: Request): Promise<Response>;
|
|
43
|
-
|
|
42
|
+
handleStream(request: Request): Promise<Response>;
|
|
44
43
|
handleAbort(request: Request): Promise<Response>;
|
|
45
44
|
}
|
|
46
45
|
type NcpHttpAgentHandlerOptions = {
|
|
47
46
|
agentClientEndpoint: NcpAgentClientEndpoint;
|
|
48
47
|
/**
|
|
49
|
-
* Optional. When set, `/
|
|
50
|
-
* See
|
|
48
|
+
* Optional. When set, `/stream` serves stored events instead of forwarding to the agent.
|
|
49
|
+
* See NcpHttpAgentStreamProvider for details.
|
|
51
50
|
*/
|
|
52
|
-
|
|
51
|
+
streamProvider?: NcpHttpAgentStreamProvider;
|
|
53
52
|
timeoutMs: number;
|
|
54
53
|
};
|
|
55
54
|
|
|
56
55
|
/**
|
|
57
56
|
* Framework-agnostic controller for NCP agent HTTP routes.
|
|
58
|
-
* Forwards /send and /
|
|
57
|
+
* Forwards /send and /stream to agentClientEndpoint; /stream uses streamProvider when set.
|
|
59
58
|
*/
|
|
60
59
|
declare class NcpHttpAgentController implements NcpHttpAgentHandler {
|
|
61
60
|
private readonly options;
|
|
62
61
|
constructor(options: NcpHttpAgentHandlerOptions);
|
|
63
62
|
handleSend(request: Request): Promise<Response>;
|
|
64
|
-
|
|
63
|
+
handleStream(request: Request): Promise<Response>;
|
|
65
64
|
handleAbort(request: Request): Promise<Response>;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
declare function createNcpHttpAgentRouter(options: NcpHttpAgentServerOptions): Hono;
|
|
69
68
|
declare function mountNcpHttpAgentRoutes(app: Hono, options: NcpHttpAgentServerOptions): void;
|
|
70
69
|
|
|
71
|
-
export { NcpHttpAgentController, type NcpHttpAgentHandler, type NcpHttpAgentHandlerOptions, type
|
|
70
|
+
export { NcpHttpAgentController, type NcpHttpAgentHandler, type NcpHttpAgentHandlerOptions, type NcpHttpAgentServerOptions, type NcpHttpAgentStreamProvider, createNcpHttpAgentRouter, mountNcpHttpAgentRoutes };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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
6
|
var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
@@ -34,37 +37,33 @@ async function parseRequestEnvelope(request) {
|
|
|
34
37
|
return null;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
|
-
function
|
|
40
|
+
function parseStreamPayloadFromUrl(url) {
|
|
38
41
|
const query = new URL(url).searchParams;
|
|
39
42
|
const sessionId = query.get("sessionId")?.trim();
|
|
40
|
-
|
|
41
|
-
if (!sessionId || !remoteRunId) {
|
|
43
|
+
if (!sessionId) {
|
|
42
44
|
return null;
|
|
43
45
|
}
|
|
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
46
|
return {
|
|
47
|
-
sessionId
|
|
48
|
-
remoteRunId,
|
|
49
|
-
...typeof fromEventIndex === "number" ? { fromEventIndex } : {}
|
|
47
|
+
sessionId
|
|
50
48
|
};
|
|
51
49
|
}
|
|
52
50
|
async function parseAbortPayload(request) {
|
|
53
51
|
try {
|
|
54
52
|
const payload = await request.json();
|
|
55
53
|
if (!isRecord(payload)) {
|
|
56
|
-
return
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const sessionId = readTrimmedString(payload.sessionId);
|
|
57
|
+
if (!sessionId) {
|
|
58
|
+
return null;
|
|
57
59
|
}
|
|
58
60
|
const messageId = readTrimmedString(payload.messageId);
|
|
59
|
-
const correlationId = readTrimmedString(payload.correlationId);
|
|
60
|
-
const runId = readTrimmedString(payload.runId);
|
|
61
61
|
return {
|
|
62
|
-
|
|
63
|
-
...
|
|
64
|
-
...runId ? { runId } : {}
|
|
62
|
+
sessionId,
|
|
63
|
+
...messageId ? { messageId } : {}
|
|
65
64
|
};
|
|
66
65
|
} catch {
|
|
67
|
-
return
|
|
66
|
+
return null;
|
|
68
67
|
}
|
|
69
68
|
}
|
|
70
69
|
function readTrimmedString(value) {
|
|
@@ -80,11 +79,10 @@ function extractScopeFromEvent(event) {
|
|
|
80
79
|
if (!payload) {
|
|
81
80
|
return {};
|
|
82
81
|
}
|
|
83
|
-
const runId = readString(payload.runId) ?? readString(payload.remoteRunId);
|
|
84
82
|
return {
|
|
85
83
|
sessionId: readString(payload.sessionId),
|
|
86
84
|
correlationId: readString(payload.correlationId),
|
|
87
|
-
runId
|
|
85
|
+
runId: readString(payload.runId)
|
|
88
86
|
};
|
|
89
87
|
}
|
|
90
88
|
function matchesScope(scope, event) {
|
|
@@ -275,6 +273,7 @@ async function* createForwardSseEvents(options) {
|
|
|
275
273
|
let timeoutId = null;
|
|
276
274
|
let unsubscribe = null;
|
|
277
275
|
let stopped = false;
|
|
276
|
+
let terminalStopScheduled = false;
|
|
278
277
|
const push = (frame) => {
|
|
279
278
|
if (!stopped) {
|
|
280
279
|
queue.push(frame);
|
|
@@ -307,12 +306,19 @@ async function* createForwardSseEvents(options) {
|
|
|
307
306
|
}
|
|
308
307
|
push(toNcpEventFrame(event));
|
|
309
308
|
if (isTerminalEvent(event)) {
|
|
310
|
-
|
|
309
|
+
if (!terminalStopScheduled) {
|
|
310
|
+
terminalStopScheduled = true;
|
|
311
|
+
queueMicrotask(stop);
|
|
312
|
+
}
|
|
311
313
|
}
|
|
312
314
|
});
|
|
313
315
|
void endpoint.emit(requestEvent).catch((error) => {
|
|
314
316
|
push(toErrorFrame("EMIT_FAILED", errorMessage(error)));
|
|
315
317
|
stop();
|
|
318
|
+
}).finally(() => {
|
|
319
|
+
if (!terminalStopScheduled) {
|
|
320
|
+
queueMicrotask(stop);
|
|
321
|
+
}
|
|
316
322
|
});
|
|
317
323
|
try {
|
|
318
324
|
for await (const frame of queue.iterable) {
|
|
@@ -322,16 +328,16 @@ async function* createForwardSseEvents(options) {
|
|
|
322
328
|
stop();
|
|
323
329
|
}
|
|
324
330
|
}
|
|
325
|
-
function
|
|
331
|
+
function createLiveStreamResponse(options) {
|
|
326
332
|
const { signal } = options;
|
|
327
333
|
return buildSseResponse(
|
|
328
|
-
createSseEventStream(
|
|
334
|
+
createSseEventStream(createLiveStreamSseEvents(options), signal)
|
|
329
335
|
);
|
|
330
336
|
}
|
|
331
|
-
async function*
|
|
332
|
-
const {
|
|
337
|
+
async function* createLiveStreamSseEvents(options) {
|
|
338
|
+
const { streamProvider, payload, signal } = options;
|
|
333
339
|
try {
|
|
334
|
-
for await (const event of
|
|
340
|
+
for await (const event of streamProvider.stream({ payload, signal })) {
|
|
335
341
|
if (signal.aborted) {
|
|
336
342
|
break;
|
|
337
343
|
}
|
|
@@ -341,7 +347,7 @@ async function* createReplaySseEvents(options) {
|
|
|
341
347
|
}
|
|
342
348
|
}
|
|
343
349
|
} catch (error) {
|
|
344
|
-
yield toErrorFrame("
|
|
350
|
+
yield toErrorFrame("STREAM_SOURCE_FAILED", errorMessage(error));
|
|
345
351
|
}
|
|
346
352
|
}
|
|
347
353
|
|
|
@@ -361,7 +367,7 @@ var NcpHttpAgentController = class {
|
|
|
361
367
|
}
|
|
362
368
|
return createForwardResponse({
|
|
363
369
|
endpoint: agentClientEndpoint,
|
|
364
|
-
requestEvent: { type:
|
|
370
|
+
requestEvent: { type: NcpEventType.MessageRequest, payload: envelope },
|
|
365
371
|
requestSignal: request.signal,
|
|
366
372
|
timeoutMs,
|
|
367
373
|
scope: {
|
|
@@ -370,36 +376,41 @@ var NcpHttpAgentController = class {
|
|
|
370
376
|
}
|
|
371
377
|
});
|
|
372
378
|
}
|
|
373
|
-
async
|
|
374
|
-
const { agentClientEndpoint,
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
379
|
+
async handleStream(request) {
|
|
380
|
+
const { agentClientEndpoint, streamProvider, timeoutMs } = this.options;
|
|
381
|
+
const streamPayload = parseStreamPayloadFromUrl(request.url);
|
|
382
|
+
if (!streamPayload) {
|
|
377
383
|
return jsonResponse(
|
|
378
|
-
{ ok: false, error: { code: "INVALID_QUERY", message: "sessionId
|
|
384
|
+
{ ok: false, error: { code: "INVALID_QUERY", message: "sessionId is required." } },
|
|
379
385
|
400
|
|
380
386
|
);
|
|
381
387
|
}
|
|
382
|
-
if (
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
payload:
|
|
388
|
+
if (streamProvider) {
|
|
389
|
+
return createLiveStreamResponse({
|
|
390
|
+
streamProvider,
|
|
391
|
+
payload: streamPayload,
|
|
386
392
|
signal: request.signal
|
|
387
393
|
});
|
|
388
394
|
}
|
|
389
395
|
return createForwardResponse({
|
|
390
396
|
endpoint: agentClientEndpoint,
|
|
391
|
-
requestEvent: { type:
|
|
397
|
+
requestEvent: { type: NcpEventType.MessageStreamRequest, payload: streamPayload },
|
|
392
398
|
requestSignal: request.signal,
|
|
393
399
|
timeoutMs,
|
|
394
400
|
scope: {
|
|
395
|
-
sessionId:
|
|
396
|
-
runId: resumePayload.remoteRunId
|
|
401
|
+
sessionId: streamPayload.sessionId
|
|
397
402
|
}
|
|
398
403
|
});
|
|
399
404
|
}
|
|
400
405
|
async handleAbort(request) {
|
|
401
406
|
const { agentClientEndpoint } = this.options;
|
|
402
407
|
const payload = await parseAbortPayload(request);
|
|
408
|
+
if (!payload) {
|
|
409
|
+
return jsonResponse(
|
|
410
|
+
{ ok: false, error: { code: "INVALID_BODY", message: "sessionId is required." } },
|
|
411
|
+
400
|
|
412
|
+
);
|
|
413
|
+
}
|
|
403
414
|
await agentClientEndpoint.abort(payload);
|
|
404
415
|
return jsonResponse({ ok: true });
|
|
405
416
|
}
|
|
@@ -413,15 +424,15 @@ function createNcpHttpAgentRouter(options) {
|
|
|
413
424
|
return app;
|
|
414
425
|
}
|
|
415
426
|
function mountNcpHttpAgentRoutes(app, options) {
|
|
416
|
-
const { basePath: rawBasePath, agentClientEndpoint,
|
|
427
|
+
const { basePath: rawBasePath, agentClientEndpoint, streamProvider, requestTimeoutMs } = options;
|
|
417
428
|
const basePath = normalizeBasePath(rawBasePath);
|
|
418
429
|
const controller = new NcpHttpAgentController({
|
|
419
430
|
agentClientEndpoint,
|
|
420
|
-
|
|
431
|
+
streamProvider,
|
|
421
432
|
timeoutMs: sanitizeTimeout(requestTimeoutMs)
|
|
422
433
|
});
|
|
423
434
|
app.post(`${basePath}/send`, (c) => controller.handleSend(c.req.raw));
|
|
424
|
-
app.get(`${basePath}/
|
|
435
|
+
app.get(`${basePath}/stream`, (c) => controller.handleStream(c.req.raw));
|
|
425
436
|
app.post(`${basePath}/abort`, (c) => controller.handleAbort(c.req.raw));
|
|
426
437
|
}
|
|
427
438
|
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.2.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.2.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",
|