@agentuity/react 0.0.30 → 0.0.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/react",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -13,7 +13,8 @@
13
13
  "files": [
14
14
  "AGENTS.md",
15
15
  "README.md",
16
- "dist"
16
+ "dist",
17
+ "src"
17
18
  ],
18
19
  "scripts": {
19
20
  "clean": "rm -rf dist",
@@ -22,7 +23,7 @@
22
23
  "prepublishOnly": "bun run clean && bun run build"
23
24
  },
24
25
  "dependencies": {
25
- "@agentuity/core": "0.0.29"
26
+ "@agentuity/core": "0.0.31"
26
27
  },
27
28
  "devDependencies": {
28
29
  "typescript": "^5.9.0"
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { createContext, type Context, type ReactElement } from 'react';
3
+ import { defaultBaseUrl } from './url';
4
+
5
+ export interface ContextProviderArgs {
6
+ children?: React.ReactNode;
7
+ baseUrl?: string;
8
+ }
9
+
10
+ export const AgentuityContext: Context<ContextProviderArgs> = createContext<ContextProviderArgs>({
11
+ baseUrl: '',
12
+ });
13
+
14
+ export const AgentuityProvider = ({ baseUrl, children }: ContextProviderArgs): ReactElement => {
15
+ return (
16
+ <AgentuityContext.Provider value={{ baseUrl: baseUrl || defaultBaseUrl }}>
17
+ {children}
18
+ </AgentuityContext.Provider>
19
+ );
20
+ };
package/src/env.ts ADDED
@@ -0,0 +1,6 @@
1
+ export const getProcessEnv = (key: string): string | undefined => {
2
+ if (typeof process !== 'undefined' && process.env) {
3
+ return process.env[key];
4
+ }
5
+ return undefined;
6
+ };
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './context';
2
+ export * from './run';
3
+ export * from './websocket';
4
+ export * from './types';
package/src/run.ts ADDED
@@ -0,0 +1,96 @@
1
+ import { useContext, useState } from 'react';
2
+ import type { InferInput, InferOutput } from '@agentuity/core';
3
+ import { buildUrl } from './url';
4
+ import { AgentuityContext } from './context';
5
+ import type { AgentName, AgentRegistry } from './types';
6
+
7
+ interface RunArgs {
8
+ /**
9
+ * Optional query parameters to append to the URL
10
+ */
11
+ query?: URLSearchParams;
12
+ /**
13
+ * Optional headers to send with the request
14
+ */
15
+ headers?: Record<string, string>;
16
+ /**
17
+ * Optional subpath to append to the agent path (such as /agent/:agent_name/:subpath)
18
+ */
19
+ subpath?: string;
20
+ /**
21
+ * HTTP method to use (default: POST)
22
+ */
23
+ method?: string;
24
+ /**
25
+ * Optional AbortSignal to cancel the request
26
+ */
27
+ signal?: AbortSignal;
28
+ }
29
+
30
+ interface UseAgentResponse<TInput, TOutput> {
31
+ data?: TOutput;
32
+ run: (input: TInput, options?: RunArgs) => Promise<TOutput>;
33
+ running: boolean;
34
+ }
35
+
36
+ export const useAgent = <
37
+ TName extends AgentName,
38
+ TInput = TName extends keyof AgentRegistry
39
+ ? InferInput<AgentRegistry[TName]['inputSchema']>
40
+ : never,
41
+ TOutput = TName extends keyof AgentRegistry
42
+ ? InferOutput<AgentRegistry[TName]['outputSchema']>
43
+ : never,
44
+ >(
45
+ name: TName
46
+ ): UseAgentResponse<TInput, TOutput> => {
47
+ const context = useContext(AgentuityContext);
48
+ const [data, setData] = useState<TOutput>();
49
+ const [running, setRunning] = useState(false);
50
+
51
+ if (!context) {
52
+ throw new Error('useAgent must be used within a AgentuityProvider');
53
+ }
54
+
55
+ const run = async (input: TInput, options?: RunArgs): Promise<TOutput> => {
56
+ setRunning(true);
57
+ try {
58
+ const url = buildUrl(context.baseUrl!, `/agent/${name}`, options?.subpath, options?.query);
59
+ const signal = options?.signal ?? new AbortController().signal;
60
+ const response = await fetch(url, {
61
+ method: options?.method ?? 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ ...(options?.headers ?? ''),
65
+ },
66
+ signal,
67
+ body:
68
+ input && typeof input === 'object' && options?.method !== 'GET'
69
+ ? JSON.stringify(input)
70
+ : undefined,
71
+ });
72
+ if (!response.ok) {
73
+ throw new Error(`Error invoking agent ${name}: ${response.statusText}`);
74
+ }
75
+ // TODO: handle streams
76
+ const ct = response.headers.get('Content-Type') || '';
77
+ if (ct.includes('text/')) {
78
+ const text = await response.text();
79
+ const _data = text as TOutput;
80
+ setData(_data);
81
+ return _data;
82
+ }
83
+ if (ct.includes('/json')) {
84
+ const data = await response.json();
85
+ const _data = data as TOutput;
86
+ setData(_data);
87
+ return _data;
88
+ }
89
+ throw new Error(`Unsupported content type: ${ct}`);
90
+ } finally {
91
+ setRunning(false);
92
+ }
93
+ };
94
+
95
+ return { data, run, running };
96
+ };
package/src/types.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { StandardSchemaV1 } from '@agentuity/core';
2
+
3
+ /**
4
+ * Agent definition interface
5
+ */
6
+ export interface Agent<
7
+ TInput extends StandardSchemaV1 = StandardSchemaV1,
8
+ TOutput extends StandardSchemaV1 = StandardSchemaV1,
9
+ > {
10
+ inputSchema: TInput;
11
+ outputSchema: TOutput;
12
+ }
13
+
14
+ /**
15
+ * Registry of all agents in the project.
16
+ * This interface is designed to be augmented by generated code in the user's project.
17
+ *
18
+ * Example usage in generated code (.agentuity/types.d.ts):
19
+ * ```typescript
20
+ * import type { Agent } from '@agentuity/react';
21
+ * import type { MyInputSchema, MyOutputSchema } from './schemas';
22
+ *
23
+ * declare module '@agentuity/react' {
24
+ * interface AgentRegistry {
25
+ * 'my-agent': Agent<MyInputSchema, MyOutputSchema>;
26
+ * 'another-agent': Agent<AnotherInput, AnotherOutput>;
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
32
+ export interface AgentRegistry {}
33
+
34
+ /**
35
+ * Union type of all registered agent names.
36
+ * Falls back to `string` when AgentRegistry is empty (before augmentation).
37
+ * After augmentation, this becomes a strict union of agent names for full type safety.
38
+ */
39
+ export type AgentName = keyof AgentRegistry extends never ? string : keyof AgentRegistry;
package/src/url.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { getProcessEnv } from './env';
2
+
3
+ export const buildUrl = (
4
+ base: string,
5
+ path: string,
6
+ subpath?: string,
7
+ query?: URLSearchParams
8
+ ): string => {
9
+ path = path.startsWith('/') ? path : `/${path}`;
10
+ let url = base.replace(/\/$/, '') + path;
11
+ if (subpath) {
12
+ subpath = subpath.startsWith('/') ? subpath : `/${subpath}`;
13
+ url += `/${subpath}`;
14
+ }
15
+ if (query) {
16
+ url += `?${query.toString()}`;
17
+ }
18
+ return url;
19
+ };
20
+
21
+ export const defaultBaseUrl: string =
22
+ getProcessEnv('NEXT_PUBLIC__AGENTUITY_URL') ||
23
+ getProcessEnv('VITE_AGENTUITY_URL') ||
24
+ getProcessEnv('AGENTUITY_URL') ||
25
+ 'http://localhost:3000';
@@ -0,0 +1,205 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import type { InferInput, InferOutput } from '@agentuity/core';
3
+ import { AgentuityContext } from './context';
4
+ import { buildUrl } from './url';
5
+ import type { AgentName, AgentRegistry } from './types';
6
+
7
+ type onMessageHandler<T = unknown> = (data: T) => void;
8
+
9
+ interface WebsocketArgs {
10
+ /**
11
+ * Optional query parameters to append to the websocket URL
12
+ */
13
+ query?: URLSearchParams;
14
+ /**
15
+ * Optional subpath to append to the agent path (such as /agent/:agent_name/:subpath)
16
+ */
17
+ subpath?: string;
18
+ /**
19
+ * Optional AbortSignal to cancel the websocket connection
20
+ */
21
+ signal?: AbortSignal;
22
+ }
23
+
24
+ const serializeWSData = (
25
+ data: unknown
26
+ ): string | ArrayBufferLike | Blob | ArrayBufferView<ArrayBufferLike> => {
27
+ if (typeof data === 'string') {
28
+ return data;
29
+ }
30
+ if (typeof data === 'object') {
31
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || data instanceof Blob) {
32
+ return data;
33
+ }
34
+ return JSON.stringify(data);
35
+ }
36
+ throw new Error('unsupported data type for websocket: ' + typeof data);
37
+ };
38
+
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
+ interface WebsocketResponse<TInput, TOutput> {
53
+ connected: boolean;
54
+ data?: TOutput;
55
+ send: (data: TInput) => void;
56
+ setHandler: (handler: onMessageHandler<TOutput>) => void;
57
+ readyState: WebSocket['readyState'];
58
+ close: () => void;
59
+ }
60
+
61
+ export const useWebsocket = <TInput, TOutput>(
62
+ path: string,
63
+ options?: WebsocketArgs
64
+ ): WebsocketResponse<TInput, TOutput> => {
65
+ const context = useContext(AgentuityContext);
66
+
67
+ if (!context) {
68
+ throw new Error('useWebsocket must be used within a AgentuityProvider');
69
+ }
70
+
71
+ const manualClose = useRef(false);
72
+ const wsRef = useRef<WebSocket | undefined>(undefined);
73
+ const pending = useRef<TOutput[]>([]);
74
+ const queued = useRef<TInput[]>([]);
75
+ const [data, setData] = useState<TOutput>();
76
+ const handler = useRef<onMessageHandler<TOutput> | undefined>(undefined);
77
+ const [connected, setConnected] = useState(false);
78
+
79
+ useEffect(() => {
80
+ if (options?.signal) {
81
+ const listener = () => {
82
+ wsRef.current?.close();
83
+ wsRef.current = undefined;
84
+ manualClose.current = true;
85
+ };
86
+ options.signal.addEventListener('abort', listener);
87
+ return () => {
88
+ options.signal?.removeEventListener('abort', listener);
89
+ };
90
+ }
91
+ }, []);
92
+
93
+ if (!wsRef.current) {
94
+ const wsUrl = buildUrl(
95
+ context.baseUrl!.replace(/^https?:/, 'ws:'),
96
+ path,
97
+ options?.subpath,
98
+ options?.query
99
+ );
100
+ const connect = () => {
101
+ wsRef.current = new WebSocket(wsUrl);
102
+ wsRef.current.onopen = () => {
103
+ setConnected(true);
104
+ if (queued.current.length > 0) {
105
+ queued.current.forEach((msg: unknown) => wsRef.current!.send(serializeWSData(msg)));
106
+ queued.current = [];
107
+ }
108
+ };
109
+ wsRef.current.onclose = () => {
110
+ wsRef.current = undefined;
111
+ queued.current = [];
112
+ setConnected(false);
113
+ if (manualClose.current) {
114
+ return;
115
+ }
116
+ setTimeout(
117
+ () => {
118
+ connect();
119
+ },
120
+ Math.random() * 500 + 500
121
+ ); // jitter reconnect
122
+ };
123
+ wsRef.current.onmessage = (event: { data: string }) => {
124
+ const payload = deserializeData<TOutput>(event.data);
125
+ setData(payload);
126
+ if (handler.current) {
127
+ handler.current(payload);
128
+ } else {
129
+ pending.current.push(payload);
130
+ }
131
+ };
132
+ };
133
+ connect();
134
+ }
135
+
136
+ const send = (data: TInput) => {
137
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
138
+ wsRef.current.send(serializeWSData(data));
139
+ } else {
140
+ queued.current.push(data);
141
+ }
142
+ };
143
+
144
+ const setHandler = (h: onMessageHandler<TOutput>) => {
145
+ handler.current = h;
146
+ pending.current.forEach(h);
147
+ pending.current = [];
148
+ };
149
+
150
+ const close = () => {
151
+ manualClose.current = true;
152
+ if (wsRef.current) {
153
+ wsRef.current.close();
154
+ wsRef.current = undefined;
155
+ }
156
+ };
157
+
158
+ return {
159
+ connected,
160
+ close,
161
+ data,
162
+ send,
163
+ setHandler,
164
+ readyState: wsRef.current?.readyState ?? WebSocket.CLOSED,
165
+ };
166
+ };
167
+
168
+ interface UseAgentWebsocketResponse<TInput, TOutput>
169
+ extends Omit<WebsocketResponse<TInput, TOutput>, 'setHandler'> {
170
+ /**
171
+ * Data received from the agent via WebSocket
172
+ */
173
+ data?: TOutput;
174
+ }
175
+
176
+ export const useAgentWebsocket = <
177
+ TName extends AgentName,
178
+ TInput = TName extends keyof AgentRegistry
179
+ ? InferInput<AgentRegistry[TName]['inputSchema']>
180
+ : never,
181
+ TOutput = TName extends keyof AgentRegistry
182
+ ? InferOutput<AgentRegistry[TName]['outputSchema']>
183
+ : never,
184
+ >(
185
+ agent: AgentName,
186
+ options?: WebsocketArgs
187
+ ): UseAgentWebsocketResponse<TInput, TOutput> => {
188
+ const [data, setData] = useState<TOutput>();
189
+ const { connected, close, send, setHandler, readyState } = useWebsocket<TInput, TOutput>(
190
+ `/agent/${agent}`,
191
+ options
192
+ );
193
+
194
+ useEffect(() => {
195
+ setHandler(setData);
196
+ }, []);
197
+
198
+ return {
199
+ connected,
200
+ close,
201
+ data,
202
+ send,
203
+ readyState,
204
+ };
205
+ };