@agentuity/react 0.0.43 → 0.0.44
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 +2 -2
- package/dist/eventstream.d.ts +36 -0
- package/dist/eventstream.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/reconnect.d.ts +22 -0
- package/dist/reconnect.d.ts.map +1 -0
- package/dist/serialization.d.ts +6 -0
- package/dist/serialization.d.ts.map +1 -0
- package/dist/websocket.d.ts +1 -1
- package/dist/websocket.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/eventstream.ts +219 -0
- package/src/index.ts +1 -0
- package/src/reconnect.ts +73 -0
- package/src/serialization.ts +16 -0
- package/src/url.ts +1 -1
- package/src/websocket.ts +104 -78
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ import { AgentuityProvider } from '@agentuity/react';
|
|
|
30
30
|
|
|
31
31
|
function App() {
|
|
32
32
|
return (
|
|
33
|
-
<AgentuityProvider baseUrl="http://localhost:
|
|
33
|
+
<AgentuityProvider baseUrl="http://localhost:3500">
|
|
34
34
|
<YourApp />
|
|
35
35
|
</AgentuityProvider>
|
|
36
36
|
);
|
|
@@ -158,4 +158,4 @@ All hooks are fully typed and will infer input/output types from your agent defi
|
|
|
158
158
|
|
|
159
159
|
## License
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
Apache 2.0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { InferOutput } from '@agentuity/core';
|
|
2
|
+
import type { AgentName, AgentRegistry } from './types';
|
|
3
|
+
type onMessageHandler<T = unknown> = (data: T) => void;
|
|
4
|
+
interface EventStreamArgs {
|
|
5
|
+
/**
|
|
6
|
+
* Optional query parameters to append to the EventStream URL
|
|
7
|
+
*/
|
|
8
|
+
query?: URLSearchParams;
|
|
9
|
+
/**
|
|
10
|
+
* Optional subpath to append to the agent path (such as /agent/:agent_name/:subpath)
|
|
11
|
+
*/
|
|
12
|
+
subpath?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional AbortSignal to cancel the EventStream connection
|
|
15
|
+
*/
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
interface EventStreamResponse<TOutput> {
|
|
19
|
+
connected: boolean;
|
|
20
|
+
data?: TOutput;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
setHandler: (handler: onMessageHandler<TOutput>) => void;
|
|
23
|
+
readyState: number;
|
|
24
|
+
close: () => void;
|
|
25
|
+
reset: () => void;
|
|
26
|
+
}
|
|
27
|
+
export declare const useEventStream: <TOutput>(path: string, options?: EventStreamArgs) => EventStreamResponse<TOutput>;
|
|
28
|
+
interface UseAgentEventStreamResponse<TOutput> extends Omit<EventStreamResponse<TOutput>, 'setHandler'> {
|
|
29
|
+
/**
|
|
30
|
+
* Data received from the agent via EventStream
|
|
31
|
+
*/
|
|
32
|
+
data?: TOutput;
|
|
33
|
+
}
|
|
34
|
+
export declare const useAgentEventStream: <TName extends AgentName, TOutput = TName extends keyof AgentRegistry ? InferOutput<AgentRegistry[TName]["outputSchema"]> : never>(agent: TName, options?: EventStreamArgs) => UseAgentEventStreamResponse<TOutput>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=eventstream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventstream.d.ts","sourceRoot":"","sources":["../src/eventstream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAIxD,KAAK,gBAAgB,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAEvD,UAAU,eAAe;IACxB;;OAEG;IACH,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,UAAU,mBAAmB,CAAC,OAAO;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,eAAO,MAAM,cAAc,GAAI,OAAO,EACrC,MAAM,MAAM,EACZ,UAAU,eAAe,KACvB,mBAAmB,CAAC,OAAO,CA+I7B,CAAC;AAEF,UAAU,2BAA2B,CAAC,OAAO,CAC5C,SAAQ,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC;IACxD;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAED,eAAO,MAAM,mBAAmB,GAC/B,KAAK,SAAS,SAAS,EACvB,OAAO,GAAG,KAAK,SAAS,MAAM,aAAa,GACxC,WAAW,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,CAAC,GACjD,KAAK,EAER,OAAO,KAAK,EACZ,UAAU,eAAe,KACvB,2BAA2B,CAAC,OAAO,CAmBrC,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ReconnectOptions {
|
|
2
|
+
onReconnect: () => void;
|
|
3
|
+
threshold?: number;
|
|
4
|
+
baseDelay?: number;
|
|
5
|
+
factor?: number;
|
|
6
|
+
maxDelay?: number;
|
|
7
|
+
jitter?: number;
|
|
8
|
+
enabled?: () => boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ReconnectManager {
|
|
11
|
+
recordFailure: () => {
|
|
12
|
+
scheduled: boolean;
|
|
13
|
+
delay: number | null;
|
|
14
|
+
};
|
|
15
|
+
recordSuccess: () => void;
|
|
16
|
+
cancel: () => void;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
dispose: () => void;
|
|
19
|
+
getAttempts: () => number;
|
|
20
|
+
}
|
|
21
|
+
export declare function createReconnectManager(opts: ReconnectOptions): ReconnectManager;
|
|
22
|
+
//# sourceMappingURL=reconnect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconnect.d.ts","sourceRoot":"","sources":["../src/reconnect.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAChC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAChC,aAAa,EAAE,MAAM;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAClE,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,EAAE,MAAM,MAAM,CAAC;CAC1B;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,gBAAgB,GAAG,gBAAgB,CAqD/E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization.d.ts","sourceRoot":"","sources":["../src/serialization.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,MAAM,MAAM,KAAG,CAWjD,CAAC"}
|
package/dist/websocket.d.ts
CHANGED
|
@@ -32,6 +32,6 @@ interface UseAgentWebsocketResponse<TInput, TOutput> extends Omit<WebsocketRespo
|
|
|
32
32
|
*/
|
|
33
33
|
data?: TOutput;
|
|
34
34
|
}
|
|
35
|
-
export declare const useAgentWebsocket: <TName extends AgentName, TInput = TName extends keyof AgentRegistry ? InferInput<AgentRegistry[TName]["inputSchema"]> : never, TOutput = TName extends keyof AgentRegistry ? InferOutput<AgentRegistry[TName]["outputSchema"]> : never>(agent:
|
|
35
|
+
export declare const useAgentWebsocket: <TName extends AgentName, TInput = TName extends keyof AgentRegistry ? InferInput<AgentRegistry[TName]["inputSchema"]> : never, TOutput = TName extends keyof AgentRegistry ? InferOutput<AgentRegistry[TName]["outputSchema"]> : never>(agent: TName, options?: WebsocketArgs) => UseAgentWebsocketResponse<TInput, TOutput>;
|
|
36
36
|
export {};
|
|
37
37
|
//# sourceMappingURL=websocket.d.ts.map
|
package/dist/websocket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG/D,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG/D,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAIxD,KAAK,gBAAgB,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAEvD,UAAU,aAAa;IACtB;;OAEG;IACH,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAiBD,UAAU,iBAAiB,CAAC,MAAM,EAAE,OAAO;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,UAAU,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACzD,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;IACpC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,eAAO,MAAM,YAAY,GAAI,MAAM,EAAE,OAAO,EAC3C,MAAM,MAAM,EACZ,UAAU,aAAa,KACrB,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAuJnC,CAAC;AAEF,UAAU,yBAAyB,CAAC,MAAM,EAAE,OAAO,CAClD,SAAQ,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,CAAC;IAC9D;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAED,eAAO,MAAM,iBAAiB,GAC7B,KAAK,SAAS,SAAS,EACvB,MAAM,GAAG,KAAK,SAAS,MAAM,aAAa,GACvC,UAAU,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,GAC/C,KAAK,EACR,OAAO,GAAG,KAAK,SAAS,MAAM,aAAa,GACxC,WAAW,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,CAAC,GACjD,KAAK,EAER,OAAO,KAAK,EACZ,UAAU,aAAa,KACrB,yBAAyB,CAAC,MAAM,EAAE,OAAO,CAoB3C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentuity/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"author": "Agentuity employees and contributors",
|
|
4
6
|
"type": "module",
|
|
5
7
|
"main": "./src/index.ts",
|
|
6
8
|
"types": "./dist/index.d.ts",
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
"prepublishOnly": "bun run clean && bun run build"
|
|
24
26
|
},
|
|
25
27
|
"dependencies": {
|
|
26
|
-
"@agentuity/core": "0.0.
|
|
28
|
+
"@agentuity/core": "0.0.43"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"typescript": "^5.9.0"
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { InferOutput } from '@agentuity/core';
|
|
3
|
+
import { AgentuityContext } from './context';
|
|
4
|
+
import { buildUrl } from './url';
|
|
5
|
+
import type { AgentName, AgentRegistry } from './types';
|
|
6
|
+
import { deserializeData } from './serialization';
|
|
7
|
+
import { createReconnectManager } from './reconnect';
|
|
8
|
+
|
|
9
|
+
type onMessageHandler<T = unknown> = (data: T) => void;
|
|
10
|
+
|
|
11
|
+
interface EventStreamArgs {
|
|
12
|
+
/**
|
|
13
|
+
* Optional query parameters to append to the EventStream URL
|
|
14
|
+
*/
|
|
15
|
+
query?: URLSearchParams;
|
|
16
|
+
/**
|
|
17
|
+
* Optional subpath to append to the agent path (such as /agent/:agent_name/:subpath)
|
|
18
|
+
*/
|
|
19
|
+
subpath?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Optional AbortSignal to cancel the EventStream connection
|
|
22
|
+
*/
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface EventStreamResponse<TOutput> {
|
|
27
|
+
connected: boolean;
|
|
28
|
+
data?: TOutput;
|
|
29
|
+
error: Error | null;
|
|
30
|
+
setHandler: (handler: onMessageHandler<TOutput>) => void;
|
|
31
|
+
readyState: number;
|
|
32
|
+
close: () => void;
|
|
33
|
+
reset: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const useEventStream = <TOutput>(
|
|
37
|
+
path: string,
|
|
38
|
+
options?: EventStreamArgs
|
|
39
|
+
): EventStreamResponse<TOutput> => {
|
|
40
|
+
const context = useContext(AgentuityContext);
|
|
41
|
+
|
|
42
|
+
if (!context) {
|
|
43
|
+
throw new Error('useEventStream must be used within a AgentuityProvider');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const manualClose = useRef(false);
|
|
47
|
+
const esRef = useRef<EventSource | undefined>(undefined);
|
|
48
|
+
const pending = useRef<TOutput[]>([]);
|
|
49
|
+
const handler = useRef<onMessageHandler<TOutput> | undefined>(undefined);
|
|
50
|
+
const reconnectManagerRef = useRef<ReturnType<typeof createReconnectManager> | undefined>(
|
|
51
|
+
undefined
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const [data, setData] = useState<TOutput>();
|
|
55
|
+
const [error, setError] = useState<Error | null>(null);
|
|
56
|
+
const [connected, setConnected] = useState(false);
|
|
57
|
+
|
|
58
|
+
const esUrl = useMemo(
|
|
59
|
+
() => buildUrl(context.baseUrl!, path, options?.subpath, options?.query),
|
|
60
|
+
[context.baseUrl, path, options?.subpath, options?.query?.toString()]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const connect = useCallback(() => {
|
|
64
|
+
if (manualClose.current) return;
|
|
65
|
+
|
|
66
|
+
esRef.current = new EventSource(esUrl);
|
|
67
|
+
let firstMessageReceived = false;
|
|
68
|
+
|
|
69
|
+
esRef.current.onopen = () => {
|
|
70
|
+
reconnectManagerRef.current?.recordSuccess();
|
|
71
|
+
setConnected(true);
|
|
72
|
+
setError(null);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
esRef.current.onerror = () => {
|
|
76
|
+
setError(new Error('EventStream error'));
|
|
77
|
+
setConnected(false);
|
|
78
|
+
|
|
79
|
+
if (manualClose.current) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = reconnectManagerRef.current?.recordFailure();
|
|
84
|
+
if (result?.scheduled) {
|
|
85
|
+
const es = esRef.current;
|
|
86
|
+
if (es) {
|
|
87
|
+
es.onopen = null;
|
|
88
|
+
es.onerror = null;
|
|
89
|
+
es.onmessage = null;
|
|
90
|
+
es.close();
|
|
91
|
+
}
|
|
92
|
+
esRef.current = undefined;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
esRef.current.onmessage = (event: MessageEvent) => {
|
|
97
|
+
if (!firstMessageReceived) {
|
|
98
|
+
reconnectManagerRef.current?.recordSuccess();
|
|
99
|
+
firstMessageReceived = true;
|
|
100
|
+
}
|
|
101
|
+
const payload = deserializeData<TOutput>(event.data);
|
|
102
|
+
setData(payload);
|
|
103
|
+
if (handler.current) {
|
|
104
|
+
handler.current(payload);
|
|
105
|
+
} else {
|
|
106
|
+
pending.current.push(payload);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}, [esUrl]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
reconnectManagerRef.current = createReconnectManager({
|
|
113
|
+
onReconnect: connect,
|
|
114
|
+
threshold: 3,
|
|
115
|
+
baseDelay: 500,
|
|
116
|
+
factor: 2,
|
|
117
|
+
maxDelay: 30000,
|
|
118
|
+
jitter: 250,
|
|
119
|
+
enabled: () => !manualClose.current,
|
|
120
|
+
});
|
|
121
|
+
return () => reconnectManagerRef.current?.dispose();
|
|
122
|
+
}, [connect]);
|
|
123
|
+
|
|
124
|
+
const cleanup = useCallback(() => {
|
|
125
|
+
manualClose.current = true;
|
|
126
|
+
reconnectManagerRef.current?.dispose();
|
|
127
|
+
const es = esRef.current;
|
|
128
|
+
if (es) {
|
|
129
|
+
es.onopen = null;
|
|
130
|
+
es.onerror = null;
|
|
131
|
+
es.onmessage = null;
|
|
132
|
+
es.close();
|
|
133
|
+
}
|
|
134
|
+
esRef.current = undefined;
|
|
135
|
+
handler.current = undefined;
|
|
136
|
+
pending.current = [];
|
|
137
|
+
setConnected(false);
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
manualClose.current = false;
|
|
142
|
+
connect();
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
cleanup();
|
|
146
|
+
};
|
|
147
|
+
}, [connect, cleanup]);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (options?.signal) {
|
|
151
|
+
const listener = () => {
|
|
152
|
+
cleanup();
|
|
153
|
+
};
|
|
154
|
+
options.signal.addEventListener('abort', listener);
|
|
155
|
+
return () => {
|
|
156
|
+
options.signal?.removeEventListener('abort', listener);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}, [options?.signal, cleanup]);
|
|
160
|
+
|
|
161
|
+
const reset = () => setError(null);
|
|
162
|
+
|
|
163
|
+
const setHandler = useCallback((h: onMessageHandler<TOutput>) => {
|
|
164
|
+
handler.current = h;
|
|
165
|
+
pending.current.forEach(h);
|
|
166
|
+
pending.current = [];
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const close = () => {
|
|
170
|
+
cleanup();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
connected,
|
|
175
|
+
close,
|
|
176
|
+
data,
|
|
177
|
+
error,
|
|
178
|
+
setHandler,
|
|
179
|
+
reset,
|
|
180
|
+
readyState: esRef.current?.readyState ?? EventSource.CLOSED,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
interface UseAgentEventStreamResponse<TOutput>
|
|
185
|
+
extends Omit<EventStreamResponse<TOutput>, 'setHandler'> {
|
|
186
|
+
/**
|
|
187
|
+
* Data received from the agent via EventStream
|
|
188
|
+
*/
|
|
189
|
+
data?: TOutput;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const useAgentEventStream = <
|
|
193
|
+
TName extends AgentName,
|
|
194
|
+
TOutput = TName extends keyof AgentRegistry
|
|
195
|
+
? InferOutput<AgentRegistry[TName]['outputSchema']>
|
|
196
|
+
: never,
|
|
197
|
+
>(
|
|
198
|
+
agent: TName,
|
|
199
|
+
options?: EventStreamArgs
|
|
200
|
+
): UseAgentEventStreamResponse<TOutput> => {
|
|
201
|
+
const [data, setData] = useState<TOutput>();
|
|
202
|
+
const { connected, close, setHandler, readyState, error, reset } = useEventStream<TOutput>(
|
|
203
|
+
`/agent/${agent}`,
|
|
204
|
+
options
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
setHandler(setData);
|
|
209
|
+
}, [agent, setHandler]);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
connected,
|
|
213
|
+
close,
|
|
214
|
+
data,
|
|
215
|
+
error,
|
|
216
|
+
reset,
|
|
217
|
+
readyState,
|
|
218
|
+
};
|
|
219
|
+
};
|
package/src/index.ts
CHANGED
package/src/reconnect.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface ReconnectOptions {
|
|
2
|
+
onReconnect: () => void;
|
|
3
|
+
threshold?: number;
|
|
4
|
+
baseDelay?: number;
|
|
5
|
+
factor?: number;
|
|
6
|
+
maxDelay?: number;
|
|
7
|
+
jitter?: number;
|
|
8
|
+
enabled?: () => boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ReconnectManager {
|
|
12
|
+
recordFailure: () => { scheduled: boolean; delay: number | null };
|
|
13
|
+
recordSuccess: () => void;
|
|
14
|
+
cancel: () => void;
|
|
15
|
+
reset: () => void;
|
|
16
|
+
dispose: () => void;
|
|
17
|
+
getAttempts: () => number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createReconnectManager(opts: ReconnectOptions): ReconnectManager {
|
|
21
|
+
let attempts = 0;
|
|
22
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
23
|
+
|
|
24
|
+
const cancel = () => {
|
|
25
|
+
if (timer) {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
timer = null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const reset = () => {
|
|
32
|
+
attempts = 0;
|
|
33
|
+
cancel();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const recordSuccess = () => reset();
|
|
37
|
+
|
|
38
|
+
const computeDelay = (attemptAfterThreshold: number) => {
|
|
39
|
+
const base = opts.baseDelay ?? 500;
|
|
40
|
+
const factor = opts.factor ?? 2;
|
|
41
|
+
const max = opts.maxDelay ?? 30000;
|
|
42
|
+
const jitterMax = opts.jitter ?? 250;
|
|
43
|
+
const backoff = Math.min(base * Math.pow(factor, attemptAfterThreshold), max);
|
|
44
|
+
const jitter = jitterMax > 0 ? Math.random() * jitterMax : 0;
|
|
45
|
+
return backoff + jitter;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const recordFailure = () => {
|
|
49
|
+
attempts += 1;
|
|
50
|
+
const threshold = opts.threshold ?? 0;
|
|
51
|
+
if (opts.enabled && !opts.enabled()) {
|
|
52
|
+
return { scheduled: false, delay: null };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (attempts - threshold >= 0) {
|
|
56
|
+
const after = Math.max(0, attempts - threshold);
|
|
57
|
+
const delay = computeDelay(after);
|
|
58
|
+
cancel();
|
|
59
|
+
timer = setTimeout(() => {
|
|
60
|
+
if (opts.enabled && !opts.enabled()) return;
|
|
61
|
+
opts.onReconnect();
|
|
62
|
+
}, delay);
|
|
63
|
+
return { scheduled: true, delay };
|
|
64
|
+
}
|
|
65
|
+
return { scheduled: false, delay: null };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const dispose = () => cancel();
|
|
69
|
+
|
|
70
|
+
const getAttempts = () => attempts;
|
|
71
|
+
|
|
72
|
+
return { recordFailure, recordSuccess, cancel, reset, dispose, getAttempts };
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deserialize data received from WebSocket or EventStream.
|
|
3
|
+
* Attempts to parse as JSON if the data looks like JSON, otherwise returns as-is.
|
|
4
|
+
*/
|
|
5
|
+
export const deserializeData = <T>(data: string): T => {
|
|
6
|
+
if (data) {
|
|
7
|
+
if (data.startsWith('{') || data.startsWith('[')) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(data) as T;
|
|
10
|
+
} catch (ex) {
|
|
11
|
+
console.error('error parsing data as JSON', ex, data);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return data as T;
|
|
16
|
+
};
|
package/src/url.ts
CHANGED
package/src/websocket.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useContext, useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { InferInput, InferOutput } from '@agentuity/core';
|
|
3
3
|
import { AgentuityContext } from './context';
|
|
4
4
|
import { buildUrl } from './url';
|
|
5
5
|
import type { AgentName, AgentRegistry } from './types';
|
|
6
|
+
import { deserializeData } from './serialization';
|
|
7
|
+
import { createReconnectManager } from './reconnect';
|
|
6
8
|
|
|
7
9
|
type onMessageHandler<T = unknown> = (data: T) => void;
|
|
8
10
|
|
|
@@ -36,19 +38,6 @@ const serializeWSData = (
|
|
|
36
38
|
throw new Error('unsupported data type for websocket: ' + typeof data);
|
|
37
39
|
};
|
|
38
40
|
|
|
39
|
-
const deserializeData = <T>(data: string): T => {
|
|
40
|
-
if (data) {
|
|
41
|
-
if (data.startsWith('{') || data.startsWith('[')) {
|
|
42
|
-
try {
|
|
43
|
-
return JSON.parse(data) as T;
|
|
44
|
-
} catch (ex) {
|
|
45
|
-
console.error('error parsing websocket data as JSON', ex, data);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return data as T;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
41
|
interface WebsocketResponse<TInput, TOutput> {
|
|
53
42
|
connected: boolean;
|
|
54
43
|
data?: TOutput;
|
|
@@ -74,77 +63,118 @@ export const useWebsocket = <TInput, TOutput>(
|
|
|
74
63
|
const wsRef = useRef<WebSocket | undefined>(undefined);
|
|
75
64
|
const pending = useRef<TOutput[]>([]);
|
|
76
65
|
const queued = useRef<TInput[]>([]);
|
|
66
|
+
const handler = useRef<onMessageHandler<TOutput> | undefined>(undefined);
|
|
67
|
+
const reconnectManagerRef = useRef<ReturnType<typeof createReconnectManager> | undefined>(
|
|
68
|
+
undefined
|
|
69
|
+
);
|
|
70
|
+
|
|
77
71
|
const [data, setData] = useState<TOutput>();
|
|
78
72
|
const [error, setError] = useState<Error | null>(null);
|
|
79
|
-
const handler = useRef<onMessageHandler<TOutput> | undefined>(undefined);
|
|
80
73
|
const [connected, setConnected] = useState(false);
|
|
81
74
|
|
|
75
|
+
const wsUrl = useMemo(() => {
|
|
76
|
+
const base = context.baseUrl!;
|
|
77
|
+
const wsBase = base.replace(/^http(s?):/, 'ws$1:');
|
|
78
|
+
return buildUrl(wsBase, path, options?.subpath, options?.query);
|
|
79
|
+
}, [context.baseUrl, path, options?.subpath, options?.query?.toString()]);
|
|
80
|
+
|
|
81
|
+
const connect = useCallback(() => {
|
|
82
|
+
if (manualClose.current) return;
|
|
83
|
+
|
|
84
|
+
wsRef.current = new WebSocket(wsUrl);
|
|
85
|
+
|
|
86
|
+
wsRef.current.onopen = () => {
|
|
87
|
+
reconnectManagerRef.current?.recordSuccess();
|
|
88
|
+
setConnected(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
if (queued.current.length > 0) {
|
|
91
|
+
queued.current.forEach((msg: unknown) => wsRef.current!.send(serializeWSData(msg)));
|
|
92
|
+
queued.current = [];
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
wsRef.current.onerror = () => {
|
|
97
|
+
setError(new Error('WebSocket error'));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
wsRef.current.onclose = (evt) => {
|
|
101
|
+
wsRef.current = undefined;
|
|
102
|
+
setConnected(false);
|
|
103
|
+
if (manualClose.current) {
|
|
104
|
+
queued.current = [];
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (evt.code !== 1000) {
|
|
108
|
+
setError(new Error(`WebSocket closed: ${evt.code} ${evt.reason || ''}`));
|
|
109
|
+
}
|
|
110
|
+
reconnectManagerRef.current?.recordFailure();
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
wsRef.current.onmessage = (event: { data: string }) => {
|
|
114
|
+
const payload = deserializeData<TOutput>(event.data);
|
|
115
|
+
setData(payload);
|
|
116
|
+
if (handler.current) {
|
|
117
|
+
handler.current(payload);
|
|
118
|
+
} else {
|
|
119
|
+
pending.current.push(payload);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}, [wsUrl]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
reconnectManagerRef.current = createReconnectManager({
|
|
126
|
+
onReconnect: connect,
|
|
127
|
+
threshold: 0,
|
|
128
|
+
baseDelay: 500,
|
|
129
|
+
factor: 2,
|
|
130
|
+
maxDelay: 30000,
|
|
131
|
+
jitter: 500,
|
|
132
|
+
enabled: () => !manualClose.current,
|
|
133
|
+
});
|
|
134
|
+
return () => reconnectManagerRef.current?.dispose();
|
|
135
|
+
}, [connect]);
|
|
136
|
+
|
|
137
|
+
const cleanup = useCallback(() => {
|
|
138
|
+
manualClose.current = true;
|
|
139
|
+
reconnectManagerRef.current?.dispose();
|
|
140
|
+
const ws = wsRef.current;
|
|
141
|
+
if (ws) {
|
|
142
|
+
ws.onopen = null;
|
|
143
|
+
ws.onerror = null;
|
|
144
|
+
ws.onclose = null;
|
|
145
|
+
ws.onmessage = null;
|
|
146
|
+
ws.close();
|
|
147
|
+
}
|
|
148
|
+
wsRef.current = undefined;
|
|
149
|
+
handler.current = undefined;
|
|
150
|
+
pending.current = [];
|
|
151
|
+
queued.current = [];
|
|
152
|
+
setConnected(false);
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
manualClose.current = false;
|
|
157
|
+
connect();
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
cleanup();
|
|
161
|
+
};
|
|
162
|
+
}, [connect, cleanup]);
|
|
163
|
+
|
|
82
164
|
useEffect(() => {
|
|
83
165
|
if (options?.signal) {
|
|
84
166
|
const listener = () => {
|
|
85
|
-
|
|
86
|
-
wsRef.current = undefined;
|
|
87
|
-
manualClose.current = true;
|
|
167
|
+
cleanup();
|
|
88
168
|
};
|
|
89
169
|
options.signal.addEventListener('abort', listener);
|
|
90
170
|
return () => {
|
|
91
171
|
options.signal?.removeEventListener('abort', listener);
|
|
92
172
|
};
|
|
93
173
|
}
|
|
94
|
-
}, []);
|
|
174
|
+
}, [options?.signal, cleanup]);
|
|
95
175
|
|
|
96
176
|
const reset = () => setError(null);
|
|
97
177
|
|
|
98
|
-
if (!wsRef.current) {
|
|
99
|
-
const wsUrl = buildUrl(
|
|
100
|
-
context.baseUrl!.replace(/^https?:/, 'ws:'),
|
|
101
|
-
path,
|
|
102
|
-
options?.subpath,
|
|
103
|
-
options?.query
|
|
104
|
-
);
|
|
105
|
-
const connect = () => {
|
|
106
|
-
wsRef.current = new WebSocket(wsUrl);
|
|
107
|
-
wsRef.current.onopen = () => {
|
|
108
|
-
setConnected(true);
|
|
109
|
-
setError(null);
|
|
110
|
-
if (queued.current.length > 0) {
|
|
111
|
-
queued.current.forEach((msg: unknown) => wsRef.current!.send(serializeWSData(msg)));
|
|
112
|
-
queued.current = [];
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
wsRef.current.onerror = () => {
|
|
116
|
-
setError(new Error('WebSocket error'));
|
|
117
|
-
};
|
|
118
|
-
wsRef.current.onclose = (evt) => {
|
|
119
|
-
wsRef.current = undefined;
|
|
120
|
-
queued.current = [];
|
|
121
|
-
setConnected(false);
|
|
122
|
-
if (manualClose.current) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (evt.code !== 1000) {
|
|
126
|
-
setError(new Error(`WebSocket closed: ${evt.code} ${evt.reason || ''}`));
|
|
127
|
-
}
|
|
128
|
-
setTimeout(
|
|
129
|
-
() => {
|
|
130
|
-
connect();
|
|
131
|
-
},
|
|
132
|
-
Math.random() * 500 + 500
|
|
133
|
-
); // jitter reconnect
|
|
134
|
-
};
|
|
135
|
-
wsRef.current.onmessage = (event: { data: string }) => {
|
|
136
|
-
const payload = deserializeData<TOutput>(event.data);
|
|
137
|
-
setData(payload);
|
|
138
|
-
if (handler.current) {
|
|
139
|
-
handler.current(payload);
|
|
140
|
-
} else {
|
|
141
|
-
pending.current.push(payload);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
};
|
|
145
|
-
connect();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
178
|
const send = (data: TInput) => {
|
|
149
179
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
150
180
|
wsRef.current.send(serializeWSData(data));
|
|
@@ -153,18 +183,14 @@ export const useWebsocket = <TInput, TOutput>(
|
|
|
153
183
|
}
|
|
154
184
|
};
|
|
155
185
|
|
|
156
|
-
const setHandler = (h: onMessageHandler<TOutput>) => {
|
|
186
|
+
const setHandler = useCallback((h: onMessageHandler<TOutput>) => {
|
|
157
187
|
handler.current = h;
|
|
158
188
|
pending.current.forEach(h);
|
|
159
189
|
pending.current = [];
|
|
160
|
-
};
|
|
190
|
+
}, []);
|
|
161
191
|
|
|
162
192
|
const close = () => {
|
|
163
|
-
|
|
164
|
-
if (wsRef.current) {
|
|
165
|
-
wsRef.current.close();
|
|
166
|
-
wsRef.current = undefined;
|
|
167
|
-
}
|
|
193
|
+
cleanup();
|
|
168
194
|
};
|
|
169
195
|
|
|
170
196
|
return {
|
|
@@ -196,7 +222,7 @@ export const useAgentWebsocket = <
|
|
|
196
222
|
? InferOutput<AgentRegistry[TName]['outputSchema']>
|
|
197
223
|
: never,
|
|
198
224
|
>(
|
|
199
|
-
agent:
|
|
225
|
+
agent: TName,
|
|
200
226
|
options?: WebsocketArgs
|
|
201
227
|
): UseAgentWebsocketResponse<TInput, TOutput> => {
|
|
202
228
|
const [data, setData] = useState<TOutput>();
|
|
@@ -207,7 +233,7 @@ export const useAgentWebsocket = <
|
|
|
207
233
|
|
|
208
234
|
useEffect(() => {
|
|
209
235
|
setHandler(setData);
|
|
210
|
-
}, []);
|
|
236
|
+
}, [agent, setHandler]);
|
|
211
237
|
|
|
212
238
|
return {
|
|
213
239
|
connected,
|