@nextclaw/ncp-http-agent-server 0.1.0 → 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 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.
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 { 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
  };
@@ -31,41 +29,42 @@ type NcpHttpAgentServerOptions = {
31
29
  basePath?: string;
32
30
  requestTimeoutMs?: number;
33
31
  /**
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.
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
- replayProvider?: NcpHttpAgentReplayProvider;
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
- handleReconnect(request: Request): Promise<Response>;
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, `/reconnect` replays stored events instead of forwarding to the agent.
50
- * See NcpHttpAgentReplayProvider for details.
48
+ * Optional. When set, `/stream` serves stored events instead of forwarding to the agent.
49
+ * See NcpHttpAgentStreamProvider for details.
51
50
  */
52
- replayProvider?: NcpHttpAgentReplayProvider;
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 /reconnect to agentClientEndpoint; /reconnect uses replayProvider when set.
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
- handleReconnect(request: Request): Promise<Response>;
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 NcpHttpAgentReplayProvider, type NcpHttpAgentServerOptions, createNcpHttpAgentRouter, mountNcpHttpAgentRoutes };
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 parseResumePayloadFromUrl(url) {
40
+ function parseStreamPayloadFromUrl(url) {
38
41
  const query = new URL(url).searchParams;
39
42
  const sessionId = query.get("sessionId")?.trim();
40
- const remoteRunId = query.get("remoteRunId")?.trim();
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
- ...messageId ? { messageId } : {},
63
- ...correlationId ? { correlationId } : {},
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
- stop();
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 createReplayResponse(options) {
331
+ function createLiveStreamResponse(options) {
326
332
  const { signal } = options;
327
333
  return buildSseResponse(
328
- createSseEventStream(createReplaySseEvents(options), signal)
334
+ createSseEventStream(createLiveStreamSseEvents(options), signal)
329
335
  );
330
336
  }
331
- async function* createReplaySseEvents(options) {
332
- const { replayProvider, payload, signal } = options;
337
+ async function* createLiveStreamSseEvents(options) {
338
+ const { streamProvider, payload, signal } = options;
333
339
  try {
334
- for await (const event of replayProvider.stream({ payload, signal })) {
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("REPLAY_FAILED", errorMessage(error));
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: "message.request", payload: envelope },
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 handleReconnect(request) {
374
- const { agentClientEndpoint, replayProvider, timeoutMs } = this.options;
375
- const resumePayload = parseResumePayloadFromUrl(request.url);
376
- if (!resumePayload) {
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 and remoteRunId are required." } },
384
+ { ok: false, error: { code: "INVALID_QUERY", message: "sessionId is required." } },
379
385
  400
380
386
  );
381
387
  }
382
- if (replayProvider) {
383
- return createReplayResponse({
384
- replayProvider,
385
- payload: resumePayload,
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: "message.resume-request", payload: resumePayload },
397
+ requestEvent: { type: NcpEventType.MessageStreamRequest, payload: streamPayload },
392
398
  requestSignal: request.signal,
393
399
  timeoutMs,
394
400
  scope: {
395
- sessionId: resumePayload.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, replayProvider, requestTimeoutMs } = options;
427
+ const { basePath: rawBasePath, agentClientEndpoint, streamProvider, requestTimeoutMs } = options;
417
428
  const basePath = normalizeBasePath(rawBasePath);
418
429
  const controller = new NcpHttpAgentController({
419
430
  agentClientEndpoint,
420
- replayProvider,
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}/reconnect`, (c) => controller.handleReconnect(c.req.raw));
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.1.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.1.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",