@agentuity/react 0.0.42 → 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 CHANGED
@@ -30,7 +30,7 @@ import { AgentuityProvider } from '@agentuity/react';
30
30
 
31
31
  function App() {
32
32
  return (
33
- <AgentuityProvider baseUrl="http://localhost:3000">
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
- MIT
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
@@ -1,5 +1,6 @@
1
1
  export * from './context';
2
2
  export * from './run';
3
3
  export * from './websocket';
4
+ export * from './eventstream';
4
5
  export * from './types';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -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,6 @@
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 declare const deserializeData: <T>(data: string) => T;
6
+ //# sourceMappingURL=serialization.d.ts.map
@@ -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"}
@@ -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: AgentName, options?: WebsocketArgs) => UseAgentWebsocketResponse<TInput, TOutput>;
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
@@ -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;AAExD,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;AA8BD,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,CAkHnC,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,SAAS,EAChB,UAAU,aAAa,KACrB,yBAAyB,CAAC,MAAM,EAAE,OAAO,CAoB3C,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.42",
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.35"
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
@@ -1,4 +1,5 @@
1
1
  export * from './context';
2
2
  export * from './run';
3
3
  export * from './websocket';
4
+ export * from './eventstream';
4
5
  export * from './types';
@@ -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
@@ -22,4 +22,4 @@ export const defaultBaseUrl: string =
22
22
  getProcessEnv('NEXT_PUBLIC__AGENTUITY_URL') ||
23
23
  getProcessEnv('VITE_AGENTUITY_URL') ||
24
24
  getProcessEnv('AGENTUITY_URL') ||
25
- 'http://localhost:3000';
25
+ 'http://localhost:3500';
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
- wsRef.current?.close();
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
- manualClose.current = true;
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: AgentName,
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,