@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 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
- - `replayProvider` (optional) — When set, `/reconnect` replays stored events instead of forwarding to the agent. Scenario: user reconnects after network drop and wants to continue watching the previous reply; with replayProvider we replay from persistence, without it we forward to the agent.
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 { NcpResumeRequestPayload, NcpEndpointEvent, NcpAgentClientEndpoint } from '@nextclaw/ncp';
1
+ import { NcpStreamRequestPayload, NcpEndpointEvent, NcpAgentClientEndpoint } from '@nextclaw/ncp';
2
2
  import { Hono } from 'hono';
3
3
 
4
4
  /**
5
- * Replays stored session events for `/reconnect`.
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 /reconnect?sessionId=xxx&remoteRunId=yyy` to
9
- * "continue watching the previous reply".
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
- * - **Replay** (with replayProvider): Do not call agent. Fetch that run's events
13
- * (message.accepted, message.text-delta, message.completed, etc.) from persistence
14
- * and stream them in order. Use when you have session/event storage and want to
15
- * avoid re-running the agent.
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` fetches events by payload.sessionId and payload.remoteRunId
20
- * from your storage and yields them in order.
17
+ * **Implementation**: `stream` attaches by payload.sessionId and yields live events
18
+ * for that active session.
21
19
  */
22
- type NcpHttpAgentReplayProvider = {
20
+ type NcpHttpAgentStreamProvider = {
23
21
  stream(params: {
24
- payload: NcpResumeRequestPayload;
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. When set, `/reconnect` replays stored events instead of forwarding to the agent.
35
- * When not set, forwards `message.resume-request` to the agent.
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
- replayProvider?: NcpHttpAgentReplayProvider;
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
- handleReconnect(request: Request): Promise<Response>;
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, `/reconnect` replays stored events instead of forwarding to the agent.
50
- * See NcpHttpAgentReplayProvider for details.
52
+ * Optional. When set, `/stream` serves stored events instead of forwarding to the agent.
53
+ * See NcpHttpAgentStreamProvider for details.
51
54
  */
52
- replayProvider?: NcpHttpAgentReplayProvider;
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 /reconnect to agentClientEndpoint; /reconnect uses replayProvider when set.
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
- handleReconnect(request: Request): Promise<Response>;
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 NcpHttpAgentReplayProvider, type NcpHttpAgentServerOptions, createNcpHttpAgentRouter, mountNcpHttpAgentRoutes };
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 DEFAULT_REQUEST_TIMEOUT_MS;
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 parseResumePayloadFromUrl(url) {
42
+ function parseStreamPayloadFromUrl(url) {
38
43
  const query = new URL(url).searchParams;
39
44
  const sessionId = query.get("sessionId")?.trim();
40
- const remoteRunId = query.get("remoteRunId")?.trim();
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
- ...messageId ? { messageId } : {},
63
- ...correlationId ? { correlationId } : {},
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
- timeoutId = setTimeout(() => {
301
- push(toErrorFrame("TIMEOUT", "NCP HTTP stream timed out before terminal event."));
302
- stop();
303
- }, timeoutMs);
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
- stop();
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 createReplayResponse(options) {
335
+ function createLiveStreamResponse(options) {
326
336
  const { signal } = options;
327
337
  return buildSseResponse(
328
- createSseEventStream(createReplaySseEvents(options), signal)
338
+ createSseEventStream(createLiveStreamSseEvents(options), signal)
329
339
  );
330
340
  }
331
- async function* createReplaySseEvents(options) {
332
- const { replayProvider, payload, signal } = options;
341
+ async function* createLiveStreamSseEvents(options) {
342
+ const { streamProvider, payload, signal } = options;
333
343
  try {
334
- for await (const event of replayProvider.stream({ payload, signal })) {
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("REPLAY_FAILED", errorMessage(error));
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: "message.request", payload: envelope },
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 handleReconnect(request) {
374
- const { agentClientEndpoint, replayProvider, timeoutMs } = this.options;
375
- const resumePayload = parseResumePayloadFromUrl(request.url);
376
- if (!resumePayload) {
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 and remoteRunId are required." } },
388
+ { ok: false, error: { code: "INVALID_QUERY", message: "sessionId is required." } },
379
389
  400
380
390
  );
381
391
  }
382
- if (replayProvider) {
383
- return createReplayResponse({
384
- replayProvider,
385
- payload: resumePayload,
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: "message.resume-request", payload: resumePayload },
401
+ requestEvent: { type: NcpEventType.MessageStreamRequest, payload: streamPayload },
392
402
  requestSignal: request.signal,
393
403
  timeoutMs,
394
404
  scope: {
395
- sessionId: resumePayload.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, replayProvider, requestTimeoutMs } = options;
431
+ const { basePath: rawBasePath, agentClientEndpoint, streamProvider, requestTimeoutMs } = options;
417
432
  const basePath = normalizeBasePath(rawBasePath);
418
433
  const controller = new NcpHttpAgentController({
419
434
  agentClientEndpoint,
420
- replayProvider,
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}/reconnect`, (c) => controller.handleReconnect(c.req.raw));
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.1.1",
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.1.1"
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",