@nextclaw/ncp-toolkit 0.4.16 → 0.5.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/dist/index.d.ts CHANGED
@@ -69,6 +69,7 @@ declare class EventPublisher {
69
69
 
70
70
  type RuntimeFactoryParams = {
71
71
  sessionId: string;
72
+ agentId?: string;
72
73
  stateManager: NcpAgentConversationStateManager;
73
74
  sessionMetadata: Record<string, unknown>;
74
75
  setSessionMetadata: (nextMetadata: Record<string, unknown>) => void;
@@ -76,22 +77,24 @@ type RuntimeFactoryParams = {
76
77
  type CreateRuntimeFn = (params: RuntimeFactoryParams) => NcpAgentRuntime;
77
78
  type AgentSessionRecord = {
78
79
  sessionId: string;
80
+ agentId?: string;
79
81
  messages: NcpMessage[];
80
82
  updatedAt: string;
81
83
  metadata?: Record<string, unknown>;
82
84
  };
83
85
  type LiveSessionExecution = {
84
86
  controller: AbortController;
85
- publisher: EventPublisher;
86
87
  requestEnvelope: NcpRequestEnvelope;
87
88
  abortHandled: boolean;
88
89
  closed: boolean;
89
90
  };
90
91
  type LiveSessionState = {
91
92
  sessionId: string;
93
+ agentId?: string;
92
94
  runtime: NcpAgentRuntime;
93
95
  stateManager: NcpAgentConversationStateManager;
94
96
  metadata: Record<string, unknown>;
97
+ publisher: EventPublisher;
95
98
  activeExecution: LiveSessionExecution | null;
96
99
  };
97
100
  interface AgentSessionStore {
@@ -124,12 +127,13 @@ declare class DefaultNcpAgentBackend implements NcpAgentServerEndpoint, NcpSessi
124
127
  private readonly sessionRegistry;
125
128
  private readonly executor;
126
129
  private readonly publisher;
130
+ private readonly sessionRealtime;
127
131
  private started;
128
132
  constructor(config: DefaultNcpAgentBackendConfig);
129
133
  start: () => Promise<void>;
130
134
  stop: () => Promise<void>;
131
135
  emit: (event: NcpEndpointEvent) => Promise<void>;
132
- subscribe: (listener: (event: NcpEndpointEvent) => void) => () => void;
136
+ subscribe: (listener: (event: NcpEndpointEvent) => void) => (() => void);
133
137
  send: (envelope: NcpRequestEnvelope, options?: NcpAgentRunSendOptions) => AsyncIterable<NcpEndpointEvent>;
134
138
  abort: (payload: NcpMessageAbortPayload) => Promise<void>;
135
139
  stream: (payloadOrParams: NcpStreamRequestPayload | {
@@ -146,18 +150,12 @@ declare class DefaultNcpAgentBackend implements NcpAgentServerEndpoint, NcpSessi
146
150
  private ensureStarted;
147
151
  private startSessionExecution;
148
152
  private finishSessionExecution;
149
- private closeExecution;
150
- private publishLiveEvent;
151
- private handleRequest;
152
- private streamToSubscribers;
153
+ private publishEndpointEvent;
153
154
  private handleAbort;
154
- private createAbortEvent;
155
155
  private persistSession;
156
156
  }
157
157
 
158
158
  declare class AgentRunExecutor {
159
- private readonly persistSession;
160
- constructor(persistSession: (sessionId: string) => Promise<void>);
161
159
  executeRun(session: LiveSessionState, envelope: NcpRequestEnvelope, controller: AbortController): AsyncGenerator<NcpEndpointEvent>;
162
160
  private publishFailure;
163
161
  }
package/dist/index.js CHANGED
@@ -702,31 +702,77 @@ async function consume(events) {
702
702
 
703
703
  // src/agent/agent-backend/agent-backend.ts
704
704
  import {
705
- NcpEventType as NcpEventType8
705
+ NcpEventType as NcpEventType6
706
706
  } from "@nextclaw/ncp";
707
707
 
708
- // src/errors/ncp-error-exception.ts
709
- var NcpErrorException = class extends Error {
710
- code;
711
- details;
712
- constructor(code, message, details) {
713
- super(message);
714
- this.name = "NcpErrorException";
715
- this.code = code;
716
- this.details = details;
708
+ // src/agent/agent-backend/event-publisher.ts
709
+ var EventPublisher = class {
710
+ listeners = /* @__PURE__ */ new Set();
711
+ closeListeners = /* @__PURE__ */ new Set();
712
+ closed = false;
713
+ subscribe(listener) {
714
+ if (this.closed) {
715
+ return () => void 0;
716
+ }
717
+ this.listeners.add(listener);
718
+ return () => {
719
+ this.listeners.delete(listener);
720
+ };
721
+ }
722
+ onClose(listener) {
723
+ if (this.closed) {
724
+ listener();
725
+ return () => void 0;
726
+ }
727
+ this.closeListeners.add(listener);
728
+ return () => {
729
+ this.closeListeners.delete(listener);
730
+ };
731
+ }
732
+ publish(event) {
733
+ if (this.closed) {
734
+ return;
735
+ }
736
+ for (const listener of this.listeners) {
737
+ listener(structuredClone(event));
738
+ }
739
+ }
740
+ close() {
741
+ if (this.closed) {
742
+ return;
743
+ }
744
+ this.closed = true;
745
+ this.listeners.clear();
746
+ for (const listener of this.closeListeners) {
747
+ listener();
748
+ }
749
+ this.closeListeners.clear();
717
750
  }
718
751
  };
719
752
 
720
753
  // src/agent/agent-backend/agent-live-session-registry.ts
754
+ function readOptionalAgentId(value) {
755
+ if (typeof value !== "string") {
756
+ return void 0;
757
+ }
758
+ const trimmed = value.trim().toLowerCase();
759
+ return trimmed.length > 0 ? trimmed : void 0;
760
+ }
761
+ function readAgentIdFromMetadata(metadata) {
762
+ return readOptionalAgentId(metadata?.agent_id) ?? readOptionalAgentId(metadata?.agentId);
763
+ }
721
764
  var AgentLiveSessionRegistry = class {
722
765
  constructor(sessionStore, createRuntime) {
723
766
  this.sessionStore = sessionStore;
724
767
  this.createRuntime = createRuntime;
725
768
  }
726
769
  sessions = /* @__PURE__ */ new Map();
727
- async ensureSession(sessionId, initialMetadata) {
770
+ ensureSession = async (sessionId, initialMetadata) => {
728
771
  const existing = this.sessions.get(sessionId);
729
772
  if (existing) {
773
+ if (!existing.agentId) {
774
+ existing.agentId = readAgentIdFromMetadata(initialMetadata) ?? existing.agentId;
775
+ }
730
776
  if (initialMetadata && Object.keys(initialMetadata).length > 0) {
731
777
  existing.metadata = {
732
778
  ...existing.metadata,
@@ -745,15 +791,19 @@ var AgentLiveSessionRegistry = class {
745
791
  ...storedSession?.metadata ? structuredClone(storedSession.metadata) : {},
746
792
  ...initialMetadata ? structuredClone(initialMetadata) : {}
747
793
  };
794
+ const sessionAgentId = readOptionalAgentId(storedSession?.agentId) ?? readAgentIdFromMetadata(initialMetadata);
748
795
  const session = {
749
796
  sessionId,
797
+ ...sessionAgentId ? { agentId: sessionAgentId } : {},
750
798
  stateManager,
751
799
  metadata: sessionMetadata,
752
800
  runtime: null,
801
+ publisher: new EventPublisher(),
753
802
  activeExecution: null
754
803
  };
755
804
  session.runtime = this.createRuntime({
756
805
  sessionId,
806
+ ...sessionAgentId ? { agentId: sessionAgentId } : {},
757
807
  stateManager,
758
808
  sessionMetadata,
759
809
  setSessionMetadata: (nextMetadata) => {
@@ -764,37 +814,87 @@ var AgentLiveSessionRegistry = class {
764
814
  });
765
815
  this.sessions.set(sessionId, session);
766
816
  return session;
767
- }
768
- getSession(sessionId) {
817
+ };
818
+ getSession = (sessionId) => {
769
819
  return this.sessions.get(sessionId) ?? null;
770
- }
771
- deleteSession(sessionId) {
820
+ };
821
+ deleteSession = (sessionId) => {
772
822
  const session = this.sessions.get(sessionId) ?? null;
773
823
  if (session) {
774
824
  this.sessions.delete(sessionId);
775
825
  }
776
826
  return session;
777
- }
778
- clear() {
827
+ };
828
+ clear = () => {
779
829
  this.sessions.clear();
780
- }
781
- listSessions() {
830
+ };
831
+ listSessions = () => {
782
832
  return [...this.sessions.values()];
783
- }
833
+ };
784
834
  };
785
835
  function cloneMessages(messages) {
786
836
  return messages.map((message) => structuredClone(message));
787
837
  }
788
838
 
839
+ // src/errors/ncp-error-exception.ts
840
+ var NcpErrorException = class extends Error {
841
+ code;
842
+ details;
843
+ constructor(code, message, details) {
844
+ super(message);
845
+ this.name = "NcpErrorException";
846
+ this.code = code;
847
+ this.details = details;
848
+ }
849
+ };
850
+
851
+ // src/agent/agent-backend/agent-backend-execution-utils.ts
852
+ function startAgentBackendSessionExecution(params) {
853
+ const { session, envelope, signal, onStatusChanged } = params;
854
+ if (session.activeExecution && !session.activeExecution.closed) {
855
+ throw new NcpErrorException(
856
+ "runtime-error",
857
+ `Session ${session.sessionId} already has an active execution.`,
858
+ { sessionId: session.sessionId }
859
+ );
860
+ }
861
+ const controller = new AbortController();
862
+ if (signal) {
863
+ signal.addEventListener("abort", () => controller.abort(), {
864
+ once: true
865
+ });
866
+ }
867
+ const execution = {
868
+ controller,
869
+ requestEnvelope: structuredClone(envelope),
870
+ abortHandled: false,
871
+ closed: false
872
+ };
873
+ session.activeExecution = execution;
874
+ onStatusChanged?.({ sessionKey: session.sessionId, status: "running" });
875
+ return execution;
876
+ }
877
+ function finishAgentBackendSessionExecution(params) {
878
+ const { session, execution, onStatusChanged } = params;
879
+ if (session.activeExecution === execution) {
880
+ session.activeExecution = null;
881
+ onStatusChanged?.({ sessionKey: session.sessionId, status: "idle" });
882
+ }
883
+ closeAgentBackendSessionExecution(execution);
884
+ }
885
+ function closeAgentBackendSessionExecution(execution) {
886
+ if (execution.closed) {
887
+ return;
888
+ }
889
+ execution.closed = true;
890
+ }
891
+
789
892
  // src/agent/agent-backend/agent-run-executor.ts
790
893
  import {
791
894
  isHiddenNcpMessage
792
895
  } from "@nextclaw/ncp";
793
896
  import { NcpEventType as NcpEventType3 } from "@nextclaw/ncp";
794
897
  var AgentRunExecutor = class {
795
- constructor(persistSession) {
796
- this.persistSession = persistSession;
797
- }
798
898
  async *executeRun(session, envelope, controller) {
799
899
  if (!isHiddenNcpMessage(envelope.message)) {
800
900
  const messageSent = {
@@ -806,7 +906,6 @@ var AgentRunExecutor = class {
806
906
  }
807
907
  };
808
908
  await session.stateManager.dispatch(messageSent);
809
- await this.persistSession(envelope.sessionId);
810
909
  yield structuredClone(messageSent);
811
910
  }
812
911
  try {
@@ -819,16 +918,17 @@ var AgentRunExecutor = class {
819
918
  },
820
919
  { signal: controller.signal }
821
920
  )) {
822
- await this.persistSession(envelope.sessionId);
823
921
  yield structuredClone(event);
824
922
  }
825
923
  } catch (error) {
826
924
  if (!controller.signal.aborted) {
827
- const runErrorEvent = await this.publishFailure(error, envelope, session);
925
+ const runErrorEvent = await this.publishFailure(
926
+ error,
927
+ envelope,
928
+ session
929
+ );
828
930
  yield structuredClone(runErrorEvent);
829
931
  }
830
- } finally {
831
- await this.persistSession(envelope.sessionId);
832
932
  }
833
933
  }
834
934
  async publishFailure(error, envelope, session) {
@@ -841,14 +941,187 @@ var AgentRunExecutor = class {
841
941
  }
842
942
  };
843
943
  await session.stateManager.dispatch(runErrorEvent);
844
- await this.persistSession(envelope.sessionId);
845
944
  return runErrorEvent;
846
945
  }
847
946
  };
848
947
 
849
- // src/agent/agent-backend/agent-backend-session-utils.ts
948
+ // src/agent/agent-backend/agent-backend-session-realtime.ts
850
949
  import { NcpEventType as NcpEventType4 } from "@nextclaw/ncp";
950
+
951
+ // src/agent/agent-backend/async-queue.ts
952
+ function createAsyncQueue() {
953
+ const items = [];
954
+ let closed = false;
955
+ let pendingResolve = null;
956
+ const iterable = {
957
+ [Symbol.asyncIterator]() {
958
+ return {
959
+ next: () => {
960
+ if (items.length > 0) {
961
+ return Promise.resolve({
962
+ value: items.shift(),
963
+ done: false
964
+ });
965
+ }
966
+ if (closed) {
967
+ return Promise.resolve({
968
+ value: void 0,
969
+ done: true
970
+ });
971
+ }
972
+ return new Promise((resolve) => {
973
+ pendingResolve = resolve;
974
+ });
975
+ }
976
+ };
977
+ }
978
+ };
979
+ return {
980
+ push(item) {
981
+ if (closed) {
982
+ return;
983
+ }
984
+ if (pendingResolve) {
985
+ const resolve = pendingResolve;
986
+ pendingResolve = null;
987
+ resolve({ value: item, done: false });
988
+ return;
989
+ }
990
+ items.push(item);
991
+ },
992
+ close() {
993
+ if (closed) {
994
+ return;
995
+ }
996
+ closed = true;
997
+ if (pendingResolve) {
998
+ const resolve = pendingResolve;
999
+ pendingResolve = null;
1000
+ resolve({
1001
+ value: void 0,
1002
+ done: true
1003
+ });
1004
+ }
1005
+ },
1006
+ iterable
1007
+ };
1008
+ }
1009
+
1010
+ // src/agent/agent-backend/agent-backend-session-realtime.ts
1011
+ var AgentBackendSessionRealtime = class {
1012
+ constructor(params) {
1013
+ this.params = params;
1014
+ }
1015
+ publishSessionEvent = async (session, event, options = {}) => {
1016
+ if (options.dispatchToStateManager) {
1017
+ await session.stateManager.dispatch(event);
1018
+ }
1019
+ this.params.publishEndpointEvent(event);
1020
+ session.publisher.publish(event);
1021
+ if (options.persistSession !== false) {
1022
+ await this.params.persistSession(session.sessionId);
1023
+ }
1024
+ };
1025
+ streamSessionEvents = (payloadOrParams, opts) => {
1026
+ return (async function* (self) {
1027
+ const payload = "payload" in payloadOrParams && "signal" in payloadOrParams ? payloadOrParams.payload : payloadOrParams;
1028
+ const signal = "payload" in payloadOrParams && "signal" in payloadOrParams ? payloadOrParams.signal : opts?.signal ?? new AbortController().signal;
1029
+ const session = await self.params.sessionRegistry.ensureSession(
1030
+ payload.sessionId
1031
+ );
1032
+ const queue = createAsyncQueue();
1033
+ const unsubscribe = session.publisher.subscribe((event) => {
1034
+ queue.push(event);
1035
+ });
1036
+ const unsubscribeClose = session.publisher.onClose(() => {
1037
+ queue.close();
1038
+ });
1039
+ const stop = () => {
1040
+ unsubscribe();
1041
+ unsubscribeClose();
1042
+ queue.close();
1043
+ signal.removeEventListener("abort", stop);
1044
+ };
1045
+ signal.addEventListener("abort", stop, { once: true });
1046
+ try {
1047
+ for await (const event of queue.iterable) {
1048
+ if (signal.aborted) {
1049
+ break;
1050
+ }
1051
+ yield event;
1052
+ }
1053
+ } finally {
1054
+ stop();
1055
+ }
1056
+ })(this);
1057
+ };
1058
+ appendMessage = async (sessionId, message) => {
1059
+ const normalizedSessionId = sessionId.trim();
1060
+ if (!normalizedSessionId) {
1061
+ return null;
1062
+ }
1063
+ let liveSession = this.params.sessionRegistry.getSession(normalizedSessionId);
1064
+ if (!liveSession) {
1065
+ const storedSession = await this.params.sessionStore.getSession(normalizedSessionId);
1066
+ if (!storedSession) {
1067
+ return null;
1068
+ }
1069
+ liveSession = await this.params.sessionRegistry.ensureSession(normalizedSessionId);
1070
+ }
1071
+ const nextMessage = {
1072
+ ...structuredClone(message),
1073
+ sessionId: normalizedSessionId
1074
+ };
1075
+ await this.publishSessionEvent(
1076
+ liveSession,
1077
+ {
1078
+ type: NcpEventType4.MessageSent,
1079
+ payload: {
1080
+ sessionId: normalizedSessionId,
1081
+ message: nextMessage
1082
+ }
1083
+ },
1084
+ {
1085
+ dispatchToStateManager: true
1086
+ }
1087
+ );
1088
+ return this.params.getSessionSummary(normalizedSessionId);
1089
+ };
1090
+ updateToolCallResult = async (sessionId, toolCallId, content) => {
1091
+ const normalizedSessionId = sessionId.trim();
1092
+ const normalizedToolCallId = toolCallId.trim();
1093
+ if (!normalizedSessionId || !normalizedToolCallId) {
1094
+ return null;
1095
+ }
1096
+ const liveSession = await this.params.sessionRegistry.ensureSession(normalizedSessionId);
1097
+ await this.publishSessionEvent(
1098
+ liveSession,
1099
+ {
1100
+ type: NcpEventType4.MessageToolCallResult,
1101
+ payload: {
1102
+ sessionId: normalizedSessionId,
1103
+ toolCallId: normalizedToolCallId,
1104
+ content
1105
+ }
1106
+ },
1107
+ {
1108
+ dispatchToStateManager: true
1109
+ }
1110
+ );
1111
+ return this.params.getSessionSummary(normalizedSessionId);
1112
+ };
1113
+ };
1114
+
1115
+ // src/agent/agent-backend/agent-backend-session-utils.ts
1116
+ import { NcpEventType as NcpEventType5 } from "@nextclaw/ncp";
851
1117
  var AUTO_SESSION_LABEL_MAX_LENGTH = 64;
1118
+ function readOptionalAgentId2(value) {
1119
+ if (typeof value !== "string") {
1120
+ return void 0;
1121
+ }
1122
+ const trimmed = value.trim().toLowerCase();
1123
+ return trimmed.length > 0 ? trimmed : void 0;
1124
+ }
852
1125
  function readMessages(snapshot) {
853
1126
  const messages = snapshot.messages.map((message) => structuredClone(message));
854
1127
  if (snapshot.streamingMessage) {
@@ -866,6 +1139,7 @@ function toSessionSummary(session, liveSession) {
866
1139
  });
867
1140
  return {
868
1141
  sessionId: session.sessionId,
1142
+ ...readOptionalAgentId2(session.agentId) ? { agentId: readOptionalAgentId2(session.agentId) } : {},
869
1143
  messageCount: session.messages.length,
870
1144
  updatedAt: session.updatedAt,
871
1145
  status: liveSession?.activeExecution ? "running" : "idle",
@@ -881,6 +1155,7 @@ function toLiveSessionSummary(session) {
881
1155
  });
882
1156
  return {
883
1157
  sessionId: session.sessionId,
1158
+ ...readOptionalAgentId2(session.agentId) ? { agentId: readOptionalAgentId2(session.agentId) } : {},
884
1159
  messageCount: messages.length,
885
1160
  updatedAt: now(),
886
1161
  status: session.activeExecution ? "running" : "idle",
@@ -934,18 +1209,21 @@ function withAutoSessionLabel(params) {
934
1209
  label: nextLabel
935
1210
  };
936
1211
  }
937
- function isTerminalEvent(event) {
938
- switch (event.type) {
939
- case NcpEventType4.MessageAbort:
940
- case NcpEventType4.RunFinished:
941
- case NcpEventType4.RunError:
942
- return true;
943
- default:
944
- return false;
945
- }
946
- }
947
1212
 
948
1213
  // src/agent/agent-backend/agent-backend-session-persistence.ts
1214
+ function readOptionalAgentId3(value) {
1215
+ if (typeof value !== "string") {
1216
+ return void 0;
1217
+ }
1218
+ const trimmed = value.trim().toLowerCase();
1219
+ return trimmed.length > 0 ? trimmed : void 0;
1220
+ }
1221
+ function readAgentIdFromMetadata2(metadata) {
1222
+ return readOptionalAgentId3(metadata?.agent_id) ?? readOptionalAgentId3(metadata?.agentId);
1223
+ }
1224
+ function resolvePersistedAgentId(params) {
1225
+ return readOptionalAgentId3(params.liveSession?.agentId) ?? readOptionalAgentId3(params.storedSession?.agentId) ?? readAgentIdFromMetadata2(params.liveSession?.metadata) ?? readAgentIdFromMetadata2(params.storedSession?.metadata);
1226
+ }
949
1227
  function buildUpdatedSessionRecord(params) {
950
1228
  const nextMetadata = params.patch.metadata === null ? {} : params.patch.metadata ? structuredClone(params.patch.metadata) : structuredClone(params.liveSession?.metadata ?? params.storedSession?.metadata ?? {});
951
1229
  if (params.liveSession) {
@@ -953,6 +1231,15 @@ function buildUpdatedSessionRecord(params) {
953
1231
  }
954
1232
  return {
955
1233
  sessionId: params.sessionId,
1234
+ ...resolvePersistedAgentId({
1235
+ liveSession: params.liveSession,
1236
+ storedSession: params.storedSession
1237
+ }) ? {
1238
+ agentId: resolvePersistedAgentId({
1239
+ liveSession: params.liveSession,
1240
+ storedSession: params.storedSession
1241
+ })
1242
+ } : {},
956
1243
  messages: params.liveSession ? readMessages(params.liveSession.stateManager.getSnapshot()) : params.storedSession?.messages.map((message) => structuredClone(message)) ?? [],
957
1244
  updatedAt: params.updatedAt,
958
1245
  metadata: nextMetadata
@@ -969,228 +1256,28 @@ function buildPersistedLiveSessionRecord(params) {
969
1256
  });
970
1257
  return {
971
1258
  sessionId: params.sessionId,
1259
+ ...readOptionalAgentId3(params.session.agentId) ?? readAgentIdFromMetadata2(params.session.metadata) ?? readAgentIdFromMetadata2(params.session.activeExecution?.requestEnvelope.metadata) ? {
1260
+ agentId: readOptionalAgentId3(params.session.agentId) ?? readAgentIdFromMetadata2(params.session.metadata) ?? readAgentIdFromMetadata2(params.session.activeExecution?.requestEnvelope.metadata)
1261
+ } : {},
972
1262
  messages,
973
1263
  updatedAt: params.updatedAt,
974
1264
  metadata
975
1265
  };
976
1266
  }
977
1267
 
978
- // src/agent/agent-backend/agent-backend-update-tool-call-result.ts
979
- import { NcpEventType as NcpEventType5 } from "@nextclaw/ncp";
980
- async function updateAgentBackendToolCallResult(params) {
981
- const normalizedSessionId = params.sessionId.trim();
982
- const normalizedToolCallId = params.toolCallId.trim();
983
- if (!normalizedSessionId || !normalizedToolCallId) {
984
- return null;
985
- }
986
- const liveSession = await params.sessionRegistry.ensureSession(normalizedSessionId);
987
- const event = {
988
- type: NcpEventType5.MessageToolCallResult,
989
- payload: {
990
- sessionId: normalizedSessionId,
991
- toolCallId: normalizedToolCallId,
992
- content: params.content
993
- }
994
- };
995
- await liveSession.stateManager.dispatch(event);
996
- params.publisher.publish(event);
997
- if (liveSession.activeExecution && !liveSession.activeExecution.closed) {
998
- liveSession.activeExecution.publisher.publish(event);
999
- }
1000
- await params.persistSession(normalizedSessionId);
1001
- return params.getSession(normalizedSessionId);
1002
- }
1003
-
1004
- // src/agent/agent-backend/agent-backend-append-message.ts
1005
- import { NcpEventType as NcpEventType6 } from "@nextclaw/ncp";
1006
- async function appendAgentBackendMessage(params) {
1007
- const normalizedSessionId = params.sessionId.trim();
1008
- if (!normalizedSessionId) {
1009
- return null;
1010
- }
1011
- let liveSession = params.sessionRegistry.getSession(normalizedSessionId);
1012
- if (!liveSession) {
1013
- const storedSession = await params.sessionStore.getSession(normalizedSessionId);
1014
- if (!storedSession) {
1015
- return null;
1016
- }
1017
- liveSession = await params.sessionRegistry.ensureSession(normalizedSessionId);
1018
- }
1019
- const nextMessage = {
1020
- ...structuredClone(params.message),
1021
- sessionId: normalizedSessionId
1022
- };
1023
- const event = {
1024
- type: NcpEventType6.MessageSent,
1025
- payload: {
1026
- sessionId: normalizedSessionId,
1027
- message: nextMessage
1028
- }
1029
- };
1030
- await liveSession.stateManager.dispatch(event);
1031
- params.publisher.publish(event);
1032
- if (liveSession.activeExecution && !liveSession.activeExecution.closed) {
1033
- liveSession.activeExecution.publisher.publish(event);
1034
- }
1035
- await params.persistSession(normalizedSessionId);
1036
- return params.getSession(normalizedSessionId);
1037
- }
1038
-
1039
- // src/agent/agent-backend/agent-backend-stream.ts
1040
- import { NcpEventType as NcpEventType7 } from "@nextclaw/ncp";
1041
-
1042
- // src/agent/agent-backend/async-queue.ts
1043
- function createAsyncQueue() {
1044
- const items = [];
1045
- let closed = false;
1046
- let pendingResolve = null;
1047
- const iterable = {
1048
- [Symbol.asyncIterator]() {
1049
- return {
1050
- next: () => {
1051
- if (items.length > 0) {
1052
- return Promise.resolve({
1053
- value: items.shift(),
1054
- done: false
1055
- });
1056
- }
1057
- if (closed) {
1058
- return Promise.resolve({
1059
- value: void 0,
1060
- done: true
1061
- });
1062
- }
1063
- return new Promise((resolve) => {
1064
- pendingResolve = resolve;
1065
- });
1066
- }
1067
- };
1068
- }
1069
- };
1070
- return {
1071
- push(item) {
1072
- if (closed) {
1073
- return;
1074
- }
1075
- if (pendingResolve) {
1076
- const resolve = pendingResolve;
1077
- pendingResolve = null;
1078
- resolve({ value: item, done: false });
1079
- return;
1080
- }
1081
- items.push(item);
1082
- },
1083
- close() {
1084
- if (closed) {
1085
- return;
1086
- }
1087
- closed = true;
1088
- if (pendingResolve) {
1089
- const resolve = pendingResolve;
1090
- pendingResolve = null;
1091
- resolve({
1092
- value: void 0,
1093
- done: true
1094
- });
1095
- }
1096
- },
1097
- iterable
1098
- };
1099
- }
1100
-
1101
- // src/agent/agent-backend/agent-backend-stream.ts
1102
- async function* streamAgentBackendExecution(params) {
1103
- const payload = "payload" in params.payloadOrParams && "signal" in params.payloadOrParams ? params.payloadOrParams.payload : params.payloadOrParams;
1104
- const signal = "payload" in params.payloadOrParams && "signal" in params.payloadOrParams ? params.payloadOrParams.signal : params.opts?.signal ?? new AbortController().signal;
1105
- const session = params.sessionRegistry.getSession(payload.sessionId);
1106
- const execution = session?.activeExecution;
1107
- if (!session || !execution || execution.closed) {
1108
- if (session) {
1109
- yield {
1110
- type: NcpEventType7.RunFinished,
1111
- payload: {
1112
- sessionId: payload.sessionId
1113
- }
1114
- };
1115
- }
1116
- return;
1117
- }
1118
- const queue = createAsyncQueue();
1119
- const unsubscribe = execution.publisher.subscribe((event) => {
1120
- queue.push(event);
1121
- });
1122
- const unsubscribeClose = execution.publisher.onClose(() => {
1123
- queue.close();
1124
- });
1125
- const stop = () => {
1126
- unsubscribe();
1127
- unsubscribeClose();
1128
- queue.close();
1129
- signal.removeEventListener("abort", stop);
1130
- };
1131
- signal.addEventListener("abort", stop, { once: true });
1132
- try {
1133
- for await (const event of queue.iterable) {
1134
- if (signal.aborted) {
1135
- break;
1136
- }
1137
- yield event;
1138
- if (isTerminalEvent(event)) {
1139
- break;
1140
- }
1141
- }
1142
- } finally {
1143
- stop();
1144
- }
1145
- }
1146
-
1147
- // src/agent/agent-backend/event-publisher.ts
1148
- var EventPublisher = class {
1149
- listeners = /* @__PURE__ */ new Set();
1150
- closeListeners = /* @__PURE__ */ new Set();
1151
- closed = false;
1152
- subscribe(listener) {
1153
- if (this.closed) {
1154
- return () => void 0;
1155
- }
1156
- this.listeners.add(listener);
1157
- return () => {
1158
- this.listeners.delete(listener);
1159
- };
1160
- }
1161
- onClose(listener) {
1162
- if (this.closed) {
1163
- listener();
1164
- return () => void 0;
1165
- }
1166
- this.closeListeners.add(listener);
1167
- return () => {
1168
- this.closeListeners.delete(listener);
1169
- };
1170
- }
1171
- publish(event) {
1172
- if (this.closed) {
1173
- return;
1174
- }
1175
- for (const listener of this.listeners) {
1176
- listener(structuredClone(event));
1177
- }
1178
- }
1179
- close() {
1180
- if (this.closed) {
1181
- return;
1182
- }
1183
- this.closed = true;
1184
- this.listeners.clear();
1185
- for (const listener of this.closeListeners) {
1186
- listener();
1187
- }
1188
- this.closeListeners.clear();
1189
- }
1190
- };
1191
-
1192
1268
  // src/agent/agent-backend/agent-backend.ts
1193
- var DEFAULT_SUPPORTED_PART_TYPES = ["text", "file", "source", "step-start", "reasoning", "tool-invocation", "card", "rich-text", "action", "extension"];
1269
+ var DEFAULT_SUPPORTED_PART_TYPES = [
1270
+ "text",
1271
+ "file",
1272
+ "source",
1273
+ "step-start",
1274
+ "reasoning",
1275
+ "tool-invocation",
1276
+ "card",
1277
+ "rich-text",
1278
+ "action",
1279
+ "extension"
1280
+ ];
1194
1281
  var DefaultNcpAgentBackend = class {
1195
1282
  manifest;
1196
1283
  sessionStore;
@@ -1198,13 +1285,24 @@ var DefaultNcpAgentBackend = class {
1198
1285
  sessionRegistry;
1199
1286
  executor;
1200
1287
  publisher;
1288
+ sessionRealtime;
1201
1289
  started = false;
1202
1290
  constructor(config) {
1203
1291
  this.sessionStore = config.sessionStore;
1204
1292
  this.onSessionRunStatusChanged = config.onSessionRunStatusChanged;
1205
- this.sessionRegistry = new AgentLiveSessionRegistry(this.sessionStore, config.createRuntime);
1206
- this.executor = new AgentRunExecutor(async (sessionId) => this.persistSession(sessionId));
1293
+ this.sessionRegistry = new AgentLiveSessionRegistry(
1294
+ this.sessionStore,
1295
+ config.createRuntime
1296
+ );
1297
+ this.executor = new AgentRunExecutor();
1207
1298
  this.publisher = new EventPublisher();
1299
+ this.sessionRealtime = new AgentBackendSessionRealtime({
1300
+ sessionRegistry: this.sessionRegistry,
1301
+ sessionStore: this.sessionStore,
1302
+ publishEndpointEvent: (event) => this.publishEndpointEvent(event),
1303
+ persistSession: (sessionId) => this.persistSession(sessionId),
1304
+ getSessionSummary: (sessionId) => this.getSession(sessionId)
1305
+ });
1208
1306
  this.manifest = {
1209
1307
  endpointKind: "agent",
1210
1308
  endpointId: config.endpointId?.trim() || "ncp-agent-backend",
@@ -1223,7 +1321,7 @@ var DefaultNcpAgentBackend = class {
1223
1321
  return;
1224
1322
  }
1225
1323
  this.started = true;
1226
- this.publisher.publish({ type: NcpEventType8.EndpointReady });
1324
+ this.publisher.publish({ type: NcpEventType6.EndpointReady });
1227
1325
  };
1228
1326
  stop = async () => {
1229
1327
  if (!this.started) {
@@ -1233,24 +1331,28 @@ var DefaultNcpAgentBackend = class {
1233
1331
  for (const session of this.sessionRegistry.listSessions()) {
1234
1332
  const execution = session.activeExecution;
1235
1333
  if (!execution) {
1334
+ session.publisher.close();
1236
1335
  continue;
1237
1336
  }
1238
1337
  execution.abortHandled = true;
1239
1338
  execution.controller.abort();
1240
1339
  this.finishSessionExecution(session, execution);
1340
+ session.publisher.close();
1241
1341
  }
1242
1342
  this.sessionRegistry.clear();
1243
1343
  };
1244
1344
  emit = async (event) => {
1245
1345
  await this.ensureStarted();
1246
1346
  switch (event.type) {
1247
- case NcpEventType8.MessageRequest:
1248
- await this.handleRequest(event.payload);
1347
+ case NcpEventType6.MessageRequest:
1348
+ for await (const emittedEvent of this.send(event.payload)) {
1349
+ void emittedEvent;
1350
+ }
1249
1351
  return;
1250
- case NcpEventType8.MessageStreamRequest:
1251
- await this.streamToSubscribers(event.payload);
1352
+ case NcpEventType6.MessageStreamRequest:
1353
+ await this.ensureStarted();
1252
1354
  return;
1253
- case NcpEventType8.MessageAbort:
1355
+ case NcpEventType6.MessageAbort:
1254
1356
  await this.handleAbort(event.payload);
1255
1357
  return;
1256
1358
  default:
@@ -1278,13 +1380,20 @@ var DefaultNcpAgentBackend = class {
1278
1380
  envelope,
1279
1381
  execution.controller
1280
1382
  )) {
1281
- self.publishLiveEvent(execution, event);
1383
+ await self.sessionRealtime.publishSessionEvent(session, event);
1282
1384
  yield event;
1283
1385
  }
1284
1386
  if (execution.controller.signal.aborted && !execution.abortHandled) {
1285
- const abortEvent = await self.createAbortEvent(session.sessionId);
1387
+ const abortEvent = {
1388
+ type: NcpEventType6.MessageAbort,
1389
+ payload: {
1390
+ sessionId: session.sessionId
1391
+ }
1392
+ };
1286
1393
  execution.abortHandled = true;
1287
- self.publishLiveEvent(execution, abortEvent);
1394
+ await self.sessionRealtime.publishSessionEvent(session, abortEvent, {
1395
+ dispatchToStateManager: true
1396
+ });
1288
1397
  yield abortEvent;
1289
1398
  }
1290
1399
  } finally {
@@ -1296,15 +1405,14 @@ var DefaultNcpAgentBackend = class {
1296
1405
  abort = async (payload) => {
1297
1406
  await this.handleAbort(payload);
1298
1407
  };
1299
- stream = (payloadOrParams, opts) => streamAgentBackendExecution({
1300
- payloadOrParams,
1301
- opts,
1302
- sessionRegistry: this.sessionRegistry
1303
- });
1408
+ stream = (payloadOrParams, opts) => this.sessionRealtime.streamSessionEvents(payloadOrParams, opts);
1304
1409
  listSessions = async () => {
1305
1410
  const storedSessions = await this.sessionStore.listSessions();
1306
1411
  const summaries = storedSessions.map(
1307
- (session) => toSessionSummary(session, this.sessionRegistry.getSession(session.sessionId))
1412
+ (session) => toSessionSummary(
1413
+ session,
1414
+ this.sessionRegistry.getSession(session.sessionId)
1415
+ )
1308
1416
  );
1309
1417
  for (const liveSession of this.sessionRegistry.listSessions()) {
1310
1418
  if (summaries.some((session) => session.sessionId === liveSession.sessionId)) {
@@ -1312,11 +1420,14 @@ var DefaultNcpAgentBackend = class {
1312
1420
  }
1313
1421
  summaries.push(toLiveSessionSummary(liveSession));
1314
1422
  }
1315
- return summaries.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1423
+ return summaries.sort(
1424
+ (left, right) => right.updatedAt.localeCompare(left.updatedAt)
1425
+ );
1316
1426
  };
1317
1427
  listSessionMessages = async (sessionId) => {
1318
1428
  const liveSession = this.sessionRegistry.getSession(sessionId);
1319
- if (liveSession) return readMessages(liveSession.stateManager.getSnapshot());
1429
+ if (liveSession)
1430
+ return readMessages(liveSession.stateManager.getSnapshot());
1320
1431
  const session = await this.sessionStore.getSession(sessionId);
1321
1432
  return session ? session.messages.map((message) => structuredClone(message)) : [];
1322
1433
  };
@@ -1327,39 +1438,29 @@ var DefaultNcpAgentBackend = class {
1327
1438
  };
1328
1439
  appendMessage = async (sessionId, message) => {
1329
1440
  await this.ensureStarted();
1330
- return appendAgentBackendMessage({
1331
- sessionId,
1332
- message,
1333
- sessionRegistry: this.sessionRegistry,
1334
- sessionStore: this.sessionStore,
1335
- publisher: this.publisher,
1336
- persistSession: async (nextSessionId) => this.persistSession(nextSessionId),
1337
- getSession: async (nextSessionId) => this.getSession(nextSessionId)
1338
- });
1441
+ return this.sessionRealtime.appendMessage(sessionId, message);
1339
1442
  };
1340
1443
  updateToolCallResult = async (sessionId, toolCallId, content) => {
1341
1444
  await this.ensureStarted();
1342
- return updateAgentBackendToolCallResult({
1445
+ return this.sessionRealtime.updateToolCallResult(
1343
1446
  sessionId,
1344
1447
  toolCallId,
1345
- content,
1346
- sessionRegistry: this.sessionRegistry,
1347
- publisher: this.publisher,
1348
- persistSession: async (nextSessionId) => this.persistSession(nextSessionId),
1349
- getSession: async (nextSessionId) => this.getSession(nextSessionId)
1350
- });
1448
+ content
1449
+ );
1351
1450
  };
1352
1451
  updateSession = async (sessionId, patch) => {
1353
1452
  const liveSession = this.sessionRegistry.getSession(sessionId);
1354
1453
  const storedSession = await this.sessionStore.getSession(sessionId);
1355
1454
  if (!liveSession && !storedSession) return null;
1356
- await this.sessionStore.replaceSession(buildUpdatedSessionRecord({
1357
- sessionId,
1358
- patch,
1359
- liveSession,
1360
- storedSession,
1361
- updatedAt: now()
1362
- }));
1455
+ await this.sessionStore.replaceSession(
1456
+ buildUpdatedSessionRecord({
1457
+ sessionId,
1458
+ patch,
1459
+ liveSession,
1460
+ storedSession,
1461
+ updatedAt: now()
1462
+ })
1463
+ );
1363
1464
  return this.getSession(sessionId);
1364
1465
  };
1365
1466
  deleteSession = async (sessionId) => {
@@ -1368,8 +1469,9 @@ var DefaultNcpAgentBackend = class {
1368
1469
  if (execution) {
1369
1470
  execution.abortHandled = true;
1370
1471
  execution.controller.abort();
1371
- this.closeExecution(execution);
1472
+ closeAgentBackendSessionExecution(execution);
1372
1473
  }
1474
+ liveSession?.publisher.close();
1373
1475
  await this.sessionStore.deleteSession(sessionId);
1374
1476
  };
1375
1477
  ensureStarted = async () => {
@@ -1377,61 +1479,19 @@ var DefaultNcpAgentBackend = class {
1377
1479
  await this.start();
1378
1480
  }
1379
1481
  };
1380
- startSessionExecution = (session, envelope, signal) => {
1381
- if (session.activeExecution && !session.activeExecution.closed) {
1382
- throw new NcpErrorException(
1383
- "runtime-error",
1384
- `Session ${session.sessionId} already has an active execution.`,
1385
- { sessionId: session.sessionId }
1386
- );
1387
- }
1388
- const controller = new AbortController();
1389
- if (signal) {
1390
- signal.addEventListener("abort", () => controller.abort(), {
1391
- once: true
1392
- });
1393
- }
1394
- const execution = {
1395
- controller,
1396
- publisher: new EventPublisher(),
1397
- requestEnvelope: structuredClone(envelope),
1398
- abortHandled: false,
1399
- closed: false
1400
- };
1401
- session.activeExecution = execution;
1402
- this.onSessionRunStatusChanged?.({ sessionKey: session.sessionId, status: "running" });
1403
- return execution;
1404
- };
1405
- finishSessionExecution = (session, execution) => {
1406
- if (session.activeExecution === execution) {
1407
- session.activeExecution = null;
1408
- this.onSessionRunStatusChanged?.({ sessionKey: session.sessionId, status: "idle" });
1409
- }
1410
- this.closeExecution(execution);
1411
- };
1412
- closeExecution = (execution) => {
1413
- if (execution.closed) {
1414
- return;
1415
- }
1416
- execution.closed = true;
1417
- execution.publisher.close();
1418
- };
1419
- publishLiveEvent = (execution, event) => {
1482
+ startSessionExecution = (session, envelope, signal) => startAgentBackendSessionExecution({
1483
+ session,
1484
+ envelope,
1485
+ signal,
1486
+ onStatusChanged: this.onSessionRunStatusChanged
1487
+ });
1488
+ finishSessionExecution = (session, execution) => finishAgentBackendSessionExecution({
1489
+ session,
1490
+ execution,
1491
+ onStatusChanged: this.onSessionRunStatusChanged
1492
+ });
1493
+ publishEndpointEvent = (event) => {
1420
1494
  this.publisher.publish(event);
1421
- if (!execution.closed) {
1422
- execution.publisher.publish(event);
1423
- }
1424
- };
1425
- handleRequest = async (envelope) => {
1426
- for await (const event of this.send(envelope)) {
1427
- void event;
1428
- }
1429
- };
1430
- streamToSubscribers = async (payload) => {
1431
- const signal = new AbortController().signal;
1432
- for await (const event of this.stream({ payload, signal })) {
1433
- void event;
1434
- }
1435
1495
  };
1436
1496
  handleAbort = async (payload) => {
1437
1497
  const session = this.sessionRegistry.getSession(payload.sessionId);
@@ -1441,33 +1501,28 @@ var DefaultNcpAgentBackend = class {
1441
1501
  }
1442
1502
  execution.abortHandled = true;
1443
1503
  execution.controller.abort();
1444
- const abortEvent = await this.createAbortEvent(payload.sessionId, payload.messageId);
1445
- this.publishLiveEvent(execution, abortEvent);
1446
- this.finishSessionExecution(session, execution);
1447
- };
1448
- createAbortEvent = async (sessionId, messageId) => {
1449
1504
  const abortEvent = {
1450
- type: NcpEventType8.MessageAbort,
1505
+ type: NcpEventType6.MessageAbort,
1451
1506
  payload: {
1452
- sessionId,
1453
- ...messageId ? { messageId } : {}
1507
+ sessionId: payload.sessionId,
1508
+ ...payload.messageId ? { messageId: payload.messageId } : {}
1454
1509
  }
1455
1510
  };
1456
- const liveSession = this.sessionRegistry.getSession(sessionId);
1457
- if (liveSession) {
1458
- await liveSession.stateManager.dispatch(abortEvent);
1459
- }
1460
- await this.persistSession(sessionId);
1461
- return abortEvent;
1511
+ await this.sessionRealtime.publishSessionEvent(session, abortEvent, {
1512
+ dispatchToStateManager: true
1513
+ });
1514
+ this.finishSessionExecution(session, execution);
1462
1515
  };
1463
1516
  persistSession = async (sessionId) => {
1464
1517
  const session = this.sessionRegistry.getSession(sessionId);
1465
1518
  if (!session) return;
1466
- await this.sessionStore.saveSession(buildPersistedLiveSessionRecord({
1467
- sessionId,
1468
- session,
1469
- updatedAt: now()
1470
- }));
1519
+ await this.sessionStore.saveSession(
1520
+ buildPersistedLiveSessionRecord({
1521
+ sessionId,
1522
+ session,
1523
+ updatedAt: now()
1524
+ })
1525
+ );
1471
1526
  };
1472
1527
  };
1473
1528
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ncp-toolkit",
3
- "version": "0.4.16",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "Toolkit implementations built on top of the NextClaw Communication Protocol.",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@nextclaw/ncp": "0.4.6"
18
+ "@nextclaw/ncp": "0.5.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.17.6",
@@ -23,7 +23,7 @@
23
23
  "tsup": "^8.3.5",
24
24
  "typescript": "^5.6.3",
25
25
  "vitest": "^4.1.2",
26
- "@nextclaw/ncp-agent-runtime": "0.3.6"
26
+ "@nextclaw/ncp-agent-runtime": "0.3.7"
27
27
  },
28
28
  "scripts": {
29
29
  "build": "tsup src/index.ts --format esm --dts --out-dir dist",