@nextclaw/ncp-react 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/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { NcpAgentClientEndpoint, NcpAgentConversationSnapshot, NcpMessage } from '@nextclaw/ncp';
1
+ import { NcpAgentConversationSnapshot, NcpMessage, NcpAgentClientEndpoint } from '@nextclaw/ncp';
2
2
 
3
- declare function useNcpAgent(sessionId: string, client: NcpAgentClientEndpoint): {
3
+ type UseNcpAgentResult = {
4
4
  snapshot: NcpAgentConversationSnapshot;
5
5
  visibleMessages: readonly NcpMessage[];
6
6
  activeRunId: string | null;
@@ -11,4 +11,24 @@ declare function useNcpAgent(sessionId: string, client: NcpAgentClientEndpoint):
11
11
  streamRun: () => Promise<void>;
12
12
  };
13
13
 
14
- export { useNcpAgent };
14
+ type NcpConversationSeed = {
15
+ messages: readonly NcpMessage[];
16
+ status: "idle" | "running";
17
+ };
18
+ type NcpConversationSeedLoader = (sessionId: string, signal: AbortSignal) => Promise<NcpConversationSeed>;
19
+ type UseHydratedNcpAgentOptions = {
20
+ sessionId: string;
21
+ client: NcpAgentClientEndpoint;
22
+ loadSeed: NcpConversationSeedLoader;
23
+ autoResumeRunningSession?: boolean;
24
+ };
25
+ type UseHydratedNcpAgentResult = UseNcpAgentResult & {
26
+ isHydrating: boolean;
27
+ hydrateError: Error | null;
28
+ reloadSeed: () => Promise<void>;
29
+ };
30
+ declare function useHydratedNcpAgent({ sessionId, client, loadSeed, autoResumeRunningSession, }: UseHydratedNcpAgentOptions): UseHydratedNcpAgentResult;
31
+
32
+ declare function useNcpAgent(sessionId: string, client: NcpAgentClientEndpoint): UseNcpAgentResult;
33
+
34
+ export { type NcpConversationSeed, type NcpConversationSeedLoader, type UseHydratedNcpAgentOptions, type UseHydratedNcpAgentResult, type UseNcpAgentResult, useHydratedNcpAgent, useNcpAgent };
package/dist/index.js CHANGED
@@ -1,41 +1,56 @@
1
- // src/hooks/use-ncp-agent.ts
2
- import { useEffect, useRef, useState } from "react";
1
+ // src/hooks/use-hydrated-ncp-agent.ts
2
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
3
+
4
+ // src/hooks/use-ncp-agent-runtime.ts
5
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
3
6
  import { DefaultNcpAgentConversationStateManager } from "@nextclaw/ncp-toolkit";
4
7
  import "@nextclaw/ncp";
5
- function useNcpAgent(sessionId, client) {
8
+ function shouldDispatchEventToSession(event, sessionId) {
9
+ const payload = "payload" in event ? event.payload : null;
10
+ if (!payload || typeof payload !== "object") {
11
+ return true;
12
+ }
13
+ if (!("sessionId" in payload) || typeof payload.sessionId !== "string") {
14
+ return true;
15
+ }
16
+ return payload.sessionId === sessionId;
17
+ }
18
+ function useScopedAgentManager(sessionId) {
6
19
  const managerRef = useRef();
7
- if (!managerRef.current) {
8
- managerRef.current = new DefaultNcpAgentConversationStateManager();
20
+ if (!managerRef.current || managerRef.current.sessionId !== sessionId) {
21
+ managerRef.current = {
22
+ sessionId,
23
+ manager: new DefaultNcpAgentConversationStateManager()
24
+ };
9
25
  }
10
- const [snapshot, setSnapshot] = useState(
11
- () => managerRef.current.getSnapshot()
26
+ return managerRef.current.manager;
27
+ }
28
+ function useNcpAgentRuntime({
29
+ sessionId,
30
+ client,
31
+ manager
32
+ }) {
33
+ const snapshot = useSyncExternalStore(
34
+ (onStoreChange) => manager.subscribe(() => onStoreChange()),
35
+ () => manager.getSnapshot(),
36
+ () => manager.getSnapshot()
12
37
  );
13
38
  const [isSending, setIsSending] = useState(false);
14
39
  useEffect(() => {
15
- const manager = managerRef.current;
16
- if (!manager) {
17
- return;
18
- }
19
- const unsubscribe = manager.subscribe((nextSnapshot) => {
20
- setSnapshot(nextSnapshot);
21
- });
22
- return () => {
23
- unsubscribe();
24
- };
25
- }, []);
40
+ setIsSending(false);
41
+ }, [sessionId]);
26
42
  useEffect(() => {
27
- const manager = managerRef.current;
28
- if (!manager) {
29
- return;
30
- }
31
43
  const unsubscribeClient = client.subscribe((event) => {
44
+ if (!shouldDispatchEventToSession(event, sessionId)) {
45
+ return;
46
+ }
32
47
  void manager.dispatch(event);
33
48
  });
34
49
  return () => {
35
50
  unsubscribeClient();
36
51
  void client.stop();
37
52
  };
38
- }, [client]);
53
+ }, [client, manager, sessionId]);
39
54
  const visibleMessages = snapshot.streamingMessage ? [...snapshot.messages, snapshot.streamingMessage] : snapshot.messages;
40
55
  const activeRunId = snapshot.activeRun?.runId ?? null;
41
56
  const isRunning = !!snapshot.activeRun;
@@ -61,17 +76,16 @@ function useNcpAgent(sessionId, client) {
61
76
  }
62
77
  };
63
78
  const abort = async () => {
64
- const runId = snapshot.activeRun?.runId;
65
- if (!runId) {
79
+ if (!snapshot.activeRun) {
66
80
  return;
67
81
  }
68
- await client.abort({ runId });
82
+ await client.abort({ sessionId });
69
83
  };
70
84
  const streamRun = async () => {
71
- if (!activeRunId) {
85
+ if (!snapshot.activeRun) {
72
86
  return;
73
87
  }
74
- await client.stream({ sessionId, runId: activeRunId });
88
+ await client.stream({ sessionId });
75
89
  };
76
90
  return {
77
91
  snapshot,
@@ -84,6 +98,91 @@ function useNcpAgent(sessionId, client) {
84
98
  streamRun
85
99
  };
86
100
  }
101
+
102
+ // src/hooks/use-hydrated-ncp-agent.ts
103
+ function toError(error) {
104
+ return error instanceof Error ? error : new Error(String(error));
105
+ }
106
+ function useHydratedNcpAgent({
107
+ sessionId,
108
+ client,
109
+ loadSeed,
110
+ autoResumeRunningSession = true
111
+ }) {
112
+ const manager = useScopedAgentManager(sessionId);
113
+ const runtime = useNcpAgentRuntime({ sessionId, client, manager });
114
+ const [isHydrating, setIsHydrating] = useState2(true);
115
+ const [hydrateError, setHydrateError] = useState2(null);
116
+ const loadStateRef = useRef2({ requestId: 0, controller: null });
117
+ const reloadSeed = useCallback(async () => {
118
+ loadStateRef.current.controller?.abort();
119
+ const controller = new AbortController();
120
+ const requestId = loadStateRef.current.requestId + 1;
121
+ loadStateRef.current = {
122
+ requestId,
123
+ controller
124
+ };
125
+ await client.stop();
126
+ manager.reset();
127
+ setHydrateError(null);
128
+ setIsHydrating(true);
129
+ try {
130
+ const seed = await loadSeed(sessionId, controller.signal);
131
+ if (controller.signal.aborted || loadStateRef.current.requestId !== requestId) {
132
+ return;
133
+ }
134
+ manager.hydrate({
135
+ sessionId,
136
+ messages: seed.messages,
137
+ activeRun: seed.status === "running" ? {
138
+ runId: null,
139
+ sessionId,
140
+ abortDisabledReason: null
141
+ } : null
142
+ });
143
+ setHydrateError(null);
144
+ setIsHydrating(false);
145
+ if (seed.status === "running" && autoResumeRunningSession) {
146
+ void client.stream({ sessionId }).catch((error) => {
147
+ if (loadStateRef.current.requestId !== requestId) {
148
+ return;
149
+ }
150
+ setHydrateError(toError(error));
151
+ });
152
+ }
153
+ } catch (error) {
154
+ if (controller.signal.aborted || loadStateRef.current.requestId !== requestId) {
155
+ return;
156
+ }
157
+ setHydrateError(toError(error));
158
+ setIsHydrating(false);
159
+ } finally {
160
+ if (loadStateRef.current.controller === controller) {
161
+ loadStateRef.current.controller = null;
162
+ }
163
+ }
164
+ }, [autoResumeRunningSession, client, loadSeed, manager, sessionId]);
165
+ useEffect2(() => {
166
+ void reloadSeed();
167
+ return () => {
168
+ loadStateRef.current.controller?.abort();
169
+ loadStateRef.current.controller = null;
170
+ };
171
+ }, [reloadSeed]);
172
+ return {
173
+ ...runtime,
174
+ isHydrating,
175
+ hydrateError,
176
+ reloadSeed
177
+ };
178
+ }
179
+
180
+ // src/hooks/use-ncp-agent.ts
181
+ function useNcpAgent(sessionId, client) {
182
+ const manager = useScopedAgentManager(sessionId);
183
+ return useNcpAgentRuntime({ sessionId, client, manager });
184
+ }
87
185
  export {
186
+ useHydratedNcpAgent,
88
187
  useNcpAgent
89
188
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ncp-react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "React bindings for building NCP-based agent applications.",
6
6
  "type": "module",
@@ -15,8 +15,8 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@nextclaw/ncp": "0.1.1",
19
- "@nextclaw/ncp-toolkit": "0.1.1"
18
+ "@nextclaw/ncp-toolkit": "0.2.0",
19
+ "@nextclaw/ncp": "0.2.0"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "react": "^18.0.0 || ^19.0.0"