@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 +23 -3
- package/dist/index.js +127 -28
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NcpAgentConversationSnapshot, NcpMessage, NcpAgentClientEndpoint } from '@nextclaw/ncp';
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
20
|
+
if (!managerRef.current || managerRef.current.sessionId !== sessionId) {
|
|
21
|
+
managerRef.current = {
|
|
22
|
+
sessionId,
|
|
23
|
+
manager: new DefaultNcpAgentConversationStateManager()
|
|
24
|
+
};
|
|
9
25
|
}
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
65
|
-
if (!runId) {
|
|
79
|
+
if (!snapshot.activeRun) {
|
|
66
80
|
return;
|
|
67
81
|
}
|
|
68
|
-
await client.abort({
|
|
82
|
+
await client.abort({ sessionId });
|
|
69
83
|
};
|
|
70
84
|
const streamRun = async () => {
|
|
71
|
-
if (!
|
|
85
|
+
if (!snapshot.activeRun) {
|
|
72
86
|
return;
|
|
73
87
|
}
|
|
74
|
-
await client.stream({ sessionId
|
|
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.
|
|
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.
|
|
19
|
-
"@nextclaw/ncp
|
|
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"
|