@nextclaw/ncp-react 0.1.0 → 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/dist/index.d.ts +25 -4
- package/dist/index.js +194 -42
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NcpAgentConversationSnapshot, NcpMessage, NcpRequestEnvelope, NcpAgentClientEndpoint } from '@nextclaw/ncp';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type NcpAgentSendInput = string | NcpRequestEnvelope;
|
|
4
|
+
type UseNcpAgentResult = {
|
|
4
5
|
snapshot: NcpAgentConversationSnapshot;
|
|
5
6
|
visibleMessages: readonly NcpMessage[];
|
|
6
7
|
activeRunId: string | null;
|
|
7
8
|
isRunning: boolean;
|
|
8
9
|
isSending: boolean;
|
|
9
|
-
send: (
|
|
10
|
+
send: (input: NcpAgentSendInput) => Promise<void>;
|
|
10
11
|
abort: () => Promise<void>;
|
|
11
12
|
streamRun: () => Promise<void>;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
type NcpConversationSeed = {
|
|
16
|
+
messages: readonly NcpMessage[];
|
|
17
|
+
status: "idle" | "running";
|
|
18
|
+
};
|
|
19
|
+
type NcpConversationSeedLoader = (sessionId: string, signal: AbortSignal) => Promise<NcpConversationSeed>;
|
|
20
|
+
type UseHydratedNcpAgentOptions = {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
client: NcpAgentClientEndpoint;
|
|
23
|
+
loadSeed: NcpConversationSeedLoader;
|
|
24
|
+
autoResumeRunningSession?: boolean;
|
|
25
|
+
};
|
|
26
|
+
type UseHydratedNcpAgentResult = UseNcpAgentResult & {
|
|
27
|
+
isHydrating: boolean;
|
|
28
|
+
hydrateError: Error | null;
|
|
29
|
+
reloadSeed: () => Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
declare function useHydratedNcpAgent({ sessionId, client, loadSeed, autoResumeRunningSession, }: UseHydratedNcpAgentOptions): UseHydratedNcpAgentResult;
|
|
32
|
+
|
|
33
|
+
declare function useNcpAgent(sessionId: string, client: NcpAgentClientEndpoint): UseNcpAgentResult;
|
|
34
|
+
|
|
35
|
+
export { type NcpConversationSeed, type NcpConversationSeedLoader, type UseHydratedNcpAgentOptions, type UseHydratedNcpAgentResult, type UseNcpAgentResult, useHydratedNcpAgent, useNcpAgent };
|
package/dist/index.js
CHANGED
|
@@ -1,77 +1,144 @@
|
|
|
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
|
-
import
|
|
5
|
-
|
|
7
|
+
import {
|
|
8
|
+
NcpEventType
|
|
9
|
+
} from "@nextclaw/ncp";
|
|
10
|
+
function shouldDispatchEventToSession(event, sessionId) {
|
|
11
|
+
const payload = "payload" in event ? event.payload : null;
|
|
12
|
+
if (!payload || typeof payload !== "object") {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (!("sessionId" in payload) || typeof payload.sessionId !== "string") {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return payload.sessionId === sessionId;
|
|
19
|
+
}
|
|
20
|
+
function hasMessageContent(message) {
|
|
21
|
+
return message.parts.some((part) => {
|
|
22
|
+
if (part.type === "text" || part.type === "rich-text" || part.type === "reasoning") {
|
|
23
|
+
return part.text.trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function normalizeSendEnvelope(input, sessionId) {
|
|
29
|
+
if (typeof input === "string") {
|
|
30
|
+
const content = input.trim();
|
|
31
|
+
if (!content) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
sessionId,
|
|
36
|
+
message: {
|
|
37
|
+
id: `user-${Date.now().toString(36)}`,
|
|
38
|
+
sessionId,
|
|
39
|
+
role: "user",
|
|
40
|
+
status: "final",
|
|
41
|
+
parts: [{ type: "text", text: content }],
|
|
42
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (!hasMessageContent(input.message)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
...input,
|
|
51
|
+
sessionId: input.sessionId,
|
|
52
|
+
message: {
|
|
53
|
+
...input.message,
|
|
54
|
+
sessionId: input.message.sessionId || input.sessionId
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function toHydrationPayload(sessionId, snapshot) {
|
|
59
|
+
return {
|
|
60
|
+
sessionId,
|
|
61
|
+
messages: snapshot.messages,
|
|
62
|
+
activeRun: snapshot.activeRun
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function useScopedAgentManager(sessionId) {
|
|
6
66
|
const managerRef = useRef();
|
|
7
|
-
if (!managerRef.current) {
|
|
8
|
-
managerRef.current =
|
|
67
|
+
if (!managerRef.current || managerRef.current.sessionId !== sessionId) {
|
|
68
|
+
managerRef.current = {
|
|
69
|
+
sessionId,
|
|
70
|
+
manager: new DefaultNcpAgentConversationStateManager()
|
|
71
|
+
};
|
|
9
72
|
}
|
|
10
|
-
|
|
11
|
-
|
|
73
|
+
return managerRef.current.manager;
|
|
74
|
+
}
|
|
75
|
+
function useNcpAgentRuntime({
|
|
76
|
+
sessionId,
|
|
77
|
+
client,
|
|
78
|
+
manager
|
|
79
|
+
}) {
|
|
80
|
+
const snapshot = useSyncExternalStore(
|
|
81
|
+
(onStoreChange) => manager.subscribe(() => onStoreChange()),
|
|
82
|
+
() => manager.getSnapshot(),
|
|
83
|
+
() => manager.getSnapshot()
|
|
12
84
|
);
|
|
13
85
|
const [isSending, setIsSending] = useState(false);
|
|
14
86
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const unsubscribe = manager.subscribe((nextSnapshot) => {
|
|
20
|
-
setSnapshot(nextSnapshot);
|
|
21
|
-
});
|
|
22
|
-
return () => {
|
|
23
|
-
unsubscribe();
|
|
24
|
-
};
|
|
25
|
-
}, []);
|
|
87
|
+
setIsSending(false);
|
|
88
|
+
}, [sessionId]);
|
|
26
89
|
useEffect(() => {
|
|
27
|
-
const manager = managerRef.current;
|
|
28
|
-
if (!manager) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
90
|
const unsubscribeClient = client.subscribe((event) => {
|
|
91
|
+
if (!shouldDispatchEventToSession(event, sessionId)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
32
94
|
void manager.dispatch(event);
|
|
33
95
|
});
|
|
34
96
|
return () => {
|
|
35
97
|
unsubscribeClient();
|
|
36
98
|
void client.stop();
|
|
37
99
|
};
|
|
38
|
-
}, [client]);
|
|
100
|
+
}, [client, manager, sessionId]);
|
|
39
101
|
const visibleMessages = snapshot.streamingMessage ? [...snapshot.messages, snapshot.streamingMessage] : snapshot.messages;
|
|
40
102
|
const activeRunId = snapshot.activeRun?.runId ?? null;
|
|
41
103
|
const isRunning = !!snapshot.activeRun;
|
|
42
|
-
const send = async (
|
|
43
|
-
if (
|
|
104
|
+
const send = async (input) => {
|
|
105
|
+
if (isSending || isRunning) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const envelope = normalizeSendEnvelope(input, sessionId);
|
|
109
|
+
if (!envelope) {
|
|
44
110
|
return;
|
|
45
111
|
}
|
|
46
112
|
setIsSending(true);
|
|
47
|
-
|
|
48
|
-
|
|
113
|
+
const previousSnapshot = manager.getSnapshot();
|
|
114
|
+
await manager.dispatch({
|
|
115
|
+
type: NcpEventType.MessageSent,
|
|
116
|
+
payload: {
|
|
49
117
|
sessionId,
|
|
50
|
-
message:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
118
|
+
message: envelope.message,
|
|
119
|
+
metadata: envelope.metadata
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
await client.send(envelope);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
manager.hydrate(toHydrationPayload(sessionId, previousSnapshot));
|
|
126
|
+
throw error;
|
|
59
127
|
} finally {
|
|
60
128
|
setIsSending(false);
|
|
61
129
|
}
|
|
62
130
|
};
|
|
63
131
|
const abort = async () => {
|
|
64
|
-
|
|
65
|
-
if (!runId) {
|
|
132
|
+
if (!snapshot.activeRun) {
|
|
66
133
|
return;
|
|
67
134
|
}
|
|
68
|
-
await client.abort({
|
|
135
|
+
await client.abort({ sessionId });
|
|
69
136
|
};
|
|
70
137
|
const streamRun = async () => {
|
|
71
|
-
if (!
|
|
138
|
+
if (!snapshot.activeRun) {
|
|
72
139
|
return;
|
|
73
140
|
}
|
|
74
|
-
await client.stream({ sessionId
|
|
141
|
+
await client.stream({ sessionId });
|
|
75
142
|
};
|
|
76
143
|
return {
|
|
77
144
|
snapshot,
|
|
@@ -84,6 +151,91 @@ function useNcpAgent(sessionId, client) {
|
|
|
84
151
|
streamRun
|
|
85
152
|
};
|
|
86
153
|
}
|
|
154
|
+
|
|
155
|
+
// src/hooks/use-hydrated-ncp-agent.ts
|
|
156
|
+
function toError(error) {
|
|
157
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
158
|
+
}
|
|
159
|
+
function useHydratedNcpAgent({
|
|
160
|
+
sessionId,
|
|
161
|
+
client,
|
|
162
|
+
loadSeed,
|
|
163
|
+
autoResumeRunningSession = true
|
|
164
|
+
}) {
|
|
165
|
+
const manager = useScopedAgentManager(sessionId);
|
|
166
|
+
const runtime = useNcpAgentRuntime({ sessionId, client, manager });
|
|
167
|
+
const [isHydrating, setIsHydrating] = useState2(true);
|
|
168
|
+
const [hydrateError, setHydrateError] = useState2(null);
|
|
169
|
+
const loadStateRef = useRef2({ requestId: 0, controller: null });
|
|
170
|
+
const reloadSeed = useCallback(async () => {
|
|
171
|
+
loadStateRef.current.controller?.abort();
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const requestId = loadStateRef.current.requestId + 1;
|
|
174
|
+
loadStateRef.current = {
|
|
175
|
+
requestId,
|
|
176
|
+
controller
|
|
177
|
+
};
|
|
178
|
+
await client.stop();
|
|
179
|
+
manager.reset();
|
|
180
|
+
setHydrateError(null);
|
|
181
|
+
setIsHydrating(true);
|
|
182
|
+
try {
|
|
183
|
+
const seed = await loadSeed(sessionId, controller.signal);
|
|
184
|
+
if (controller.signal.aborted || loadStateRef.current.requestId !== requestId) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
manager.hydrate({
|
|
188
|
+
sessionId,
|
|
189
|
+
messages: seed.messages,
|
|
190
|
+
activeRun: seed.status === "running" ? {
|
|
191
|
+
runId: null,
|
|
192
|
+
sessionId,
|
|
193
|
+
abortDisabledReason: null
|
|
194
|
+
} : null
|
|
195
|
+
});
|
|
196
|
+
setHydrateError(null);
|
|
197
|
+
setIsHydrating(false);
|
|
198
|
+
if (seed.status === "running" && autoResumeRunningSession) {
|
|
199
|
+
void client.stream({ sessionId }).catch((error) => {
|
|
200
|
+
if (loadStateRef.current.requestId !== requestId) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
setHydrateError(toError(error));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (controller.signal.aborted || loadStateRef.current.requestId !== requestId) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
setHydrateError(toError(error));
|
|
211
|
+
setIsHydrating(false);
|
|
212
|
+
} finally {
|
|
213
|
+
if (loadStateRef.current.controller === controller) {
|
|
214
|
+
loadStateRef.current.controller = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}, [autoResumeRunningSession, client, loadSeed, manager, sessionId]);
|
|
218
|
+
useEffect2(() => {
|
|
219
|
+
void reloadSeed();
|
|
220
|
+
return () => {
|
|
221
|
+
loadStateRef.current.controller?.abort();
|
|
222
|
+
loadStateRef.current.controller = null;
|
|
223
|
+
};
|
|
224
|
+
}, [reloadSeed]);
|
|
225
|
+
return {
|
|
226
|
+
...runtime,
|
|
227
|
+
isHydrating,
|
|
228
|
+
hydrateError,
|
|
229
|
+
reloadSeed
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/hooks/use-ncp-agent.ts
|
|
234
|
+
function useNcpAgent(sessionId, client) {
|
|
235
|
+
const manager = useScopedAgentManager(sessionId);
|
|
236
|
+
return useNcpAgentRuntime({ sessionId, client, manager });
|
|
237
|
+
}
|
|
87
238
|
export {
|
|
239
|
+
useHydratedNcpAgent,
|
|
88
240
|
useNcpAgent
|
|
89
241
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ncp-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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-toolkit": "0.
|
|
18
|
+
"@nextclaw/ncp": "0.3.0",
|
|
19
|
+
"@nextclaw/ncp-toolkit": "0.3.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"react": "^18.0.0 || ^19.0.0"
|