@agentick/react 0.0.1

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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * React integration types for Agentick.
3
+ *
4
+ * @module @agentick/react/types
5
+ */
6
+ import type { AgentickClient, ConnectionState, StreamEvent, SendInput, SessionAccessor, ClientExecutionHandle, SessionStreamEvent, ClientTransport } from "@agentick/client";
7
+ import type { ContentBlock, Message } from "@agentick/shared";
8
+ import type { ReactNode } from "react";
9
+ /**
10
+ * Transport configuration for AgentickProvider.
11
+ * Can be a built-in transport type or a custom ClientTransport instance.
12
+ */
13
+ export type TransportConfig = "sse" | "websocket" | "auto" | ClientTransport;
14
+ /**
15
+ * Configuration for AgentickProvider.
16
+ */
17
+ export interface AgentickProviderProps {
18
+ /**
19
+ * Pre-configured client instance.
20
+ * If provided, clientConfig is ignored.
21
+ */
22
+ client?: AgentickClient;
23
+ /**
24
+ * Client configuration (used if client is not provided).
25
+ */
26
+ clientConfig?: {
27
+ baseUrl: string;
28
+ /**
29
+ * Transport to use for communication.
30
+ * - "sse": HTTP/SSE transport (default for http:// and https:// URLs)
31
+ * - "websocket": WebSocket transport (default for ws:// and wss:// URLs)
32
+ * - "auto": Auto-detect based on URL scheme (default)
33
+ * - ClientTransport instance: Use a custom transport (e.g., SharedTransport for multi-tab)
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { createSharedTransport } from '@agentick/client-multiplexer';
38
+ *
39
+ * <AgentickProvider clientConfig={{
40
+ * baseUrl: 'https://api.example.com',
41
+ * transport: createSharedTransport({ baseUrl: 'https://api.example.com' }),
42
+ * }}>
43
+ * ```
44
+ */
45
+ transport?: TransportConfig;
46
+ token?: string;
47
+ withCredentials?: boolean;
48
+ timeout?: number;
49
+ paths?: {
50
+ events?: string;
51
+ send?: string;
52
+ subscribe?: string;
53
+ abort?: string;
54
+ close?: string;
55
+ channel?: string;
56
+ };
57
+ };
58
+ children?: ReactNode;
59
+ }
60
+ /**
61
+ * Context value provided by AgentickProvider.
62
+ */
63
+ export interface AgentickContextValue {
64
+ client: AgentickClient;
65
+ }
66
+ /**
67
+ * Options for useSession hook.
68
+ */
69
+ export interface UseSessionOptions {
70
+ /**
71
+ * Session ID to work with.
72
+ * If not provided, send() will use ephemeral sessions.
73
+ */
74
+ sessionId?: string;
75
+ /**
76
+ * Automatically subscribe to the session on mount.
77
+ * Requires sessionId to be provided.
78
+ * @default false
79
+ */
80
+ autoSubscribe?: boolean;
81
+ }
82
+ /**
83
+ * Return value from useSession hook.
84
+ */
85
+ export interface UseSessionResult {
86
+ /**
87
+ * Session ID (if provided in options).
88
+ */
89
+ sessionId?: string;
90
+ /**
91
+ * Whether this session is subscribed.
92
+ */
93
+ isSubscribed: boolean;
94
+ /**
95
+ * Subscribe to this session (only if sessionId provided).
96
+ */
97
+ subscribe: () => void;
98
+ /**
99
+ * Unsubscribe from this session.
100
+ */
101
+ unsubscribe: () => void;
102
+ /**
103
+ * Send a message.
104
+ *
105
+ * If sessionId was provided, sends to that session.
106
+ * Otherwise, creates an ephemeral session.
107
+ */
108
+ send: (input: string | ContentBlock | ContentBlock[] | Message | Message[] | SendInput) => ClientExecutionHandle;
109
+ /**
110
+ * Abort the session's current execution.
111
+ */
112
+ abort: (reason?: string) => Promise<void>;
113
+ /**
114
+ * Close this session.
115
+ */
116
+ close: () => Promise<void>;
117
+ /**
118
+ * Session accessor for advanced operations (channels, tool confirmations).
119
+ * Only available if sessionId was provided.
120
+ */
121
+ accessor?: SessionAccessor;
122
+ }
123
+ /**
124
+ * Options for useConnection hook.
125
+ */
126
+ export interface UseConnectionOptions {
127
+ }
128
+ /**
129
+ * Return value from useConnection hook.
130
+ */
131
+ export interface UseConnectionResult {
132
+ /**
133
+ * Current connection state.
134
+ */
135
+ state: ConnectionState;
136
+ /**
137
+ * Whether currently connected.
138
+ */
139
+ isConnected: boolean;
140
+ /**
141
+ * Whether currently connecting.
142
+ */
143
+ isConnecting: boolean;
144
+ }
145
+ /**
146
+ * Options for useEvents hook.
147
+ */
148
+ export interface UseEventsOptions {
149
+ /**
150
+ * Optional session ID to filter events for.
151
+ * If not provided, receives all events from all sessions.
152
+ */
153
+ sessionId?: string;
154
+ /**
155
+ * Optional event type filter.
156
+ * If provided, only events of these types are returned.
157
+ */
158
+ filter?: Array<StreamEvent["type"] | SessionStreamEvent["type"]>;
159
+ /**
160
+ * Whether the hook is enabled.
161
+ * If false, no event subscription is created.
162
+ * @default true
163
+ */
164
+ enabled?: boolean;
165
+ }
166
+ /**
167
+ * Return value from useEvents hook.
168
+ */
169
+ export interface UseEventsResult {
170
+ /**
171
+ * Latest event received.
172
+ */
173
+ event: StreamEvent | SessionStreamEvent | undefined;
174
+ /**
175
+ * Clear the current event.
176
+ */
177
+ clear: () => void;
178
+ }
179
+ /**
180
+ * Options for useStreamingText hook.
181
+ */
182
+ export interface UseStreamingTextOptions {
183
+ /**
184
+ * Optional session ID to filter events for.
185
+ * If not provided, receives text from all sessions.
186
+ */
187
+ sessionId?: string;
188
+ /**
189
+ * Whether the hook is enabled.
190
+ * If false, no text accumulation occurs.
191
+ * @default true
192
+ */
193
+ enabled?: boolean;
194
+ }
195
+ /**
196
+ * Return value from useStreamingText hook.
197
+ */
198
+ export interface UseStreamingTextResult {
199
+ /**
200
+ * Accumulated text from content_delta events.
201
+ */
202
+ text: string;
203
+ /**
204
+ * Whether currently streaming (between tick_start and execution_end).
205
+ */
206
+ isStreaming: boolean;
207
+ /**
208
+ * Clear the accumulated text.
209
+ */
210
+ clear: () => void;
211
+ }
212
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * React integration types for Agentick.
3
+ *
4
+ * @module @agentick/react/types
5
+ */
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@agentick/react",
3
+ "version": "0.0.1",
4
+ "description": "React hooks and components for Agentick applications",
5
+ "files": [
6
+ "dist",
7
+ "src"
8
+ ],
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "@agentick/client": "0.0.1",
20
+ "@agentick/shared": "0.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "@testing-library/react": "^16.0.0",
24
+ "@types/node": "^22.10.5",
25
+ "@types/react": "^18.3.0",
26
+ "happy-dom": "^17.0.0",
27
+ "react": "^18.3.0",
28
+ "react-dom": "^18.3.0",
29
+ "typescript": "^5.7.3",
30
+ "vitest": "^3.0.0"
31
+ },
32
+ "peerDependencies": {
33
+ "react": "^18.0.0 || ^19.0.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "dev": "tsc --watch",
38
+ "typecheck": "tsc --noEmit",
39
+ "clean": "rm -rf dist",
40
+ "test": "vitest run --config vitest.config.ts",
41
+ "test:watch": "vitest --config vitest.config.ts"
42
+ }
43
+ }
@@ -0,0 +1,426 @@
1
+ /**
2
+ * React Hooks Tests
3
+ */
4
+
5
+ import { describe, it, expect, vi } from "vitest";
6
+ import { renderHook, act } from "@testing-library/react";
7
+ import { type ReactNode } from "react";
8
+ import {
9
+ AgentickProvider,
10
+ useClient,
11
+ useSession,
12
+ useConnectionState,
13
+ useEvents,
14
+ useStreamingText,
15
+ } from "../index";
16
+ import type {
17
+ AgentickClient,
18
+ ConnectionState,
19
+ StreamEvent,
20
+ SessionStreamEvent,
21
+ } from "@agentick/client";
22
+ import { createEventBase } from "@agentick/shared/testing";
23
+
24
+ // ============================================================================
25
+ // Mock Client
26
+ // ============================================================================
27
+
28
+ import type { StreamingTextState } from "@agentick/client";
29
+
30
+ function createMockClient(): AgentickClient & {
31
+ _eventHandlers: Set<(event: SessionStreamEvent) => void>;
32
+ _stateHandlers: Set<(state: ConnectionState) => void>;
33
+ _streamingTextHandlers: Set<(state: StreamingTextState) => void>;
34
+ _emitEvent: (event: StreamEvent | SessionStreamEvent) => void;
35
+ _emitState: (state: ConnectionState) => void;
36
+ } {
37
+ const eventHandlers = new Set<(event: SessionStreamEvent) => void>();
38
+ const stateHandlers = new Set<(state: ConnectionState) => void>();
39
+ const streamingTextHandlers = new Set<(state: StreamingTextState) => void>();
40
+ let state: ConnectionState = "disconnected";
41
+ let streamingTextState: StreamingTextState = { text: "", isStreaming: false };
42
+
43
+ const emitStreamingText = (newState: StreamingTextState) => {
44
+ streamingTextState = newState;
45
+ for (const handler of streamingTextHandlers) {
46
+ handler(newState);
47
+ }
48
+ };
49
+
50
+ const createHandle = () =>
51
+ ({
52
+ sessionId: "test-session",
53
+ executionId: "exec-1",
54
+ status: "completed",
55
+ result: Promise.resolve({
56
+ response: "ok",
57
+ outputs: {},
58
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
59
+ }),
60
+ abort: vi.fn(),
61
+ queueMessage: vi.fn(),
62
+ submitToolResult: vi.fn(),
63
+ async *[Symbol.asyncIterator]() {},
64
+ }) as any;
65
+
66
+ const createAccessor = (id: string) =>
67
+ ({
68
+ sessionId: id,
69
+ isSubscribed: false,
70
+ subscribe: vi.fn(),
71
+ unsubscribe: vi.fn(),
72
+ send: vi.fn(() => createHandle()),
73
+ abort: vi.fn(async () => {}),
74
+ close: vi.fn(async () => {}),
75
+ submitToolResult: vi.fn(),
76
+ onEvent: vi.fn(() => () => {}),
77
+ onResult: vi.fn(() => () => {}),
78
+ onToolConfirmation: vi.fn(() => () => {}),
79
+ channel: vi.fn((name: string) => ({
80
+ name,
81
+ subscribe: vi.fn(() => () => {}),
82
+ publish: vi.fn(async () => {}),
83
+ request: vi.fn(async () => ({})),
84
+ })),
85
+ }) as any;
86
+
87
+ return {
88
+ _eventHandlers: eventHandlers,
89
+ _stateHandlers: stateHandlers,
90
+ _streamingTextHandlers: streamingTextHandlers,
91
+
92
+ _emitEvent(event: StreamEvent | SessionStreamEvent) {
93
+ const withSession: SessionStreamEvent = {
94
+ sessionId: "test-session",
95
+ ...(event as StreamEvent),
96
+ };
97
+ switch (event.type) {
98
+ case "tick_start":
99
+ emitStreamingText({ text: "", isStreaming: true });
100
+ break;
101
+ case "content_delta":
102
+ emitStreamingText({
103
+ text: streamingTextState.text + (event as any).delta,
104
+ isStreaming: true,
105
+ });
106
+ break;
107
+ case "tick_end":
108
+ case "execution_end":
109
+ emitStreamingText({
110
+ text: streamingTextState.text,
111
+ isStreaming: false,
112
+ });
113
+ break;
114
+ }
115
+
116
+ for (const handler of eventHandlers) {
117
+ handler(withSession);
118
+ }
119
+ },
120
+
121
+ _emitState(newState: ConnectionState) {
122
+ state = newState;
123
+ for (const handler of stateHandlers) {
124
+ handler(newState);
125
+ }
126
+ },
127
+
128
+ get state() {
129
+ return state;
130
+ },
131
+
132
+ get streamingText() {
133
+ return streamingTextState;
134
+ },
135
+
136
+ send: vi.fn(() => createHandle()),
137
+ abort: vi.fn(async () => {}),
138
+ closeSession: vi.fn(async () => {}),
139
+ session: vi.fn((id: string) => createAccessor(id)),
140
+ subscribe: vi.fn((id: string) => createAccessor(id)),
141
+
142
+ onEvent(handler: (event: SessionStreamEvent) => void) {
143
+ eventHandlers.add(handler);
144
+ return () => eventHandlers.delete(handler);
145
+ },
146
+
147
+ onConnectionChange(handler: (newState: ConnectionState) => void) {
148
+ stateHandlers.add(handler);
149
+ return () => stateHandlers.delete(handler);
150
+ },
151
+
152
+ onStreamingText(handler: (state: StreamingTextState) => void) {
153
+ streamingTextHandlers.add(handler);
154
+ handler(streamingTextState);
155
+ return () => streamingTextHandlers.delete(handler);
156
+ },
157
+
158
+ clearStreamingText() {
159
+ emitStreamingText({ text: "", isStreaming: false });
160
+ },
161
+
162
+ on: vi.fn(() => () => {}),
163
+ destroy: vi.fn(),
164
+ } as any;
165
+ }
166
+
167
+ // ============================================================================
168
+ // Test Wrapper
169
+ // ============================================================================
170
+
171
+ function createWrapper(client: AgentickClient) {
172
+ return function Wrapper({ children }: { children: ReactNode }) {
173
+ return <AgentickProvider client={client}>{children}</AgentickProvider>;
174
+ };
175
+ }
176
+
177
+ // ============================================================================
178
+ // Tests
179
+ // ============================================================================
180
+
181
+ describe("AgentickProvider", () => {
182
+ it("provides client to children", () => {
183
+ const mockClient = createMockClient();
184
+ const wrapper = createWrapper(mockClient);
185
+
186
+ const { result } = renderHook(() => useClient(), { wrapper });
187
+
188
+ expect(result.current).toBe(mockClient);
189
+ });
190
+
191
+ it("throws when used outside provider", () => {
192
+ expect(() => {
193
+ renderHook(() => useClient());
194
+ }).toThrow("useClient must be used within a AgentickProvider");
195
+ });
196
+ });
197
+
198
+ describe("useConnectionState", () => {
199
+ it("returns current connection state", () => {
200
+ const mockClient = createMockClient();
201
+ const wrapper = createWrapper(mockClient);
202
+
203
+ const { result } = renderHook(() => useConnectionState(), { wrapper });
204
+
205
+ expect(result.current).toBe("disconnected");
206
+ });
207
+
208
+ it("updates when state changes", async () => {
209
+ const mockClient = createMockClient();
210
+ const wrapper = createWrapper(mockClient);
211
+
212
+ const { result } = renderHook(() => useConnectionState(), { wrapper });
213
+
214
+ expect(result.current).toBe("disconnected");
215
+
216
+ act(() => {
217
+ mockClient._emitState("connecting");
218
+ });
219
+
220
+ expect(result.current).toBe("connecting");
221
+
222
+ act(() => {
223
+ mockClient._emitState("connected");
224
+ });
225
+
226
+ expect(result.current).toBe("connected");
227
+ });
228
+ });
229
+
230
+ describe("useSession", () => {
231
+ it("returns session methods for ephemeral sends", () => {
232
+ const mockClient = createMockClient();
233
+ const wrapper = createWrapper(mockClient);
234
+
235
+ const { result } = renderHook(() => useSession(), { wrapper });
236
+
237
+ expect(result.current.sessionId).toBeUndefined();
238
+ expect(result.current.isSubscribed).toBe(false);
239
+ expect(typeof result.current.send).toBe("function");
240
+ expect(typeof result.current.subscribe).toBe("function");
241
+ expect(typeof result.current.unsubscribe).toBe("function");
242
+ expect(typeof result.current.abort).toBe("function");
243
+ });
244
+
245
+ it("uses client.send for ephemeral sends", () => {
246
+ const mockClient = createMockClient();
247
+ const wrapper = createWrapper(mockClient);
248
+
249
+ const { result } = renderHook(() => useSession(), { wrapper });
250
+
251
+ act(() => {
252
+ result.current.send("Hello!");
253
+ });
254
+
255
+ expect(mockClient.send).toHaveBeenCalledWith("Hello!");
256
+ });
257
+
258
+ it("uses session accessor when sessionId provided", () => {
259
+ const mockClient = createMockClient();
260
+ const wrapper = createWrapper(mockClient);
261
+
262
+ const { result } = renderHook(() => useSession({ sessionId: "conv-123" }), { wrapper });
263
+
264
+ act(() => {
265
+ result.current.send("Hello!");
266
+ });
267
+
268
+ expect(mockClient.session).toHaveBeenCalledWith("conv-123");
269
+ });
270
+ });
271
+
272
+ describe("useEvents", () => {
273
+ it("receives events", () => {
274
+ const mockClient = createMockClient();
275
+ const wrapper = createWrapper(mockClient);
276
+
277
+ const { result } = renderHook(() => useEvents(), { wrapper });
278
+
279
+ expect(result.current.event).toBeUndefined();
280
+
281
+ act(() => {
282
+ mockClient._emitEvent({ type: "tick_start", tick: 1 } as any);
283
+ });
284
+
285
+ expect(result.current.event).toEqual({
286
+ type: "tick_start",
287
+ tick: 1,
288
+ sessionId: "test-session",
289
+ });
290
+ });
291
+
292
+ it("filters events by type", () => {
293
+ const mockClient = createMockClient();
294
+ const wrapper = createWrapper(mockClient);
295
+
296
+ const { result } = renderHook(() => useEvents({ filter: ["content_delta"] }), { wrapper });
297
+
298
+ act(() => {
299
+ mockClient._emitEvent({ ...createEventBase(1), type: "tick_start", tick: 1 });
300
+ });
301
+
302
+ expect(result.current.event).toBeUndefined();
303
+
304
+ act(() => {
305
+ mockClient._emitEvent({ type: "content_delta", delta: "Hello" } as any);
306
+ });
307
+
308
+ expect(result.current.event).toEqual({
309
+ type: "content_delta",
310
+ delta: "Hello",
311
+ sessionId: "test-session",
312
+ });
313
+ });
314
+
315
+ it("clears event when clear() called", () => {
316
+ const mockClient = createMockClient();
317
+ const wrapper = createWrapper(mockClient);
318
+
319
+ const { result } = renderHook(() => useEvents(), { wrapper });
320
+
321
+ act(() => {
322
+ mockClient._emitEvent({ ...createEventBase(1), type: "tick_start", tick: 1 });
323
+ });
324
+
325
+ expect(result.current.event).toBeDefined();
326
+
327
+ act(() => {
328
+ result.current.clear();
329
+ });
330
+
331
+ expect(result.current.event).toBeUndefined();
332
+ });
333
+
334
+ it("does not receive events when disabled", () => {
335
+ const mockClient = createMockClient();
336
+ const wrapper = createWrapper(mockClient);
337
+
338
+ const { result } = renderHook(() => useEvents({ enabled: false }), { wrapper });
339
+
340
+ act(() => {
341
+ mockClient._emitEvent({ ...createEventBase(1), type: "tick_start", tick: 1 });
342
+ });
343
+
344
+ expect(result.current.event).toBeUndefined();
345
+ });
346
+ });
347
+
348
+ describe("useStreamingText", () => {
349
+ it("accumulates text from content_delta events", () => {
350
+ const mockClient = createMockClient();
351
+ const wrapper = createWrapper(mockClient);
352
+
353
+ const { result } = renderHook(() => useStreamingText(), { wrapper });
354
+
355
+ expect(result.current.text).toBe("");
356
+ expect(result.current.isStreaming).toBe(false);
357
+
358
+ act(() => {
359
+ mockClient._emitEvent({ ...createEventBase(1), type: "tick_start", tick: 1 });
360
+ });
361
+
362
+ expect(result.current.text).toBe("");
363
+ expect(result.current.isStreaming).toBe(true);
364
+
365
+ act(() => {
366
+ mockClient._emitEvent({ type: "content_delta", delta: "Hello" } as any);
367
+ });
368
+
369
+ expect(result.current.text).toBe("Hello");
370
+
371
+ act(() => {
372
+ mockClient._emitEvent({ type: "content_delta", delta: " world" } as any);
373
+ });
374
+
375
+ expect(result.current.text).toBe("Hello world");
376
+
377
+ act(() => {
378
+ mockClient._emitEvent({ type: "tick_end", tick: 1 } as any);
379
+ });
380
+
381
+ expect(result.current.text).toBe("Hello world");
382
+ expect(result.current.isStreaming).toBe(false);
383
+ });
384
+
385
+ it("clears text on new tick_start", () => {
386
+ const mockClient = createMockClient();
387
+ const wrapper = createWrapper(mockClient);
388
+
389
+ const { result } = renderHook(() => useStreamingText(), { wrapper });
390
+
391
+ act(() => {
392
+ mockClient._emitEvent({ type: "tick_start", tick: 1 });
393
+ mockClient._emitEvent({ type: "content_delta", delta: "First" } as any);
394
+ mockClient._emitEvent({ type: "tick_end", tick: 1 } as any);
395
+ });
396
+
397
+ expect(result.current.text).toBe("First");
398
+
399
+ act(() => {
400
+ mockClient._emitEvent({ type: "tick_start", tick: 2 });
401
+ });
402
+
403
+ expect(result.current.text).toBe("");
404
+ });
405
+
406
+ it("clears when clear() called", () => {
407
+ const mockClient = createMockClient();
408
+ const wrapper = createWrapper(mockClient);
409
+
410
+ const { result } = renderHook(() => useStreamingText(), { wrapper });
411
+
412
+ act(() => {
413
+ mockClient._emitEvent({ type: "tick_start", tick: 1 });
414
+ mockClient._emitEvent({ type: "content_delta", delta: "Hello" } as any);
415
+ });
416
+
417
+ expect(result.current.text).toBe("Hello");
418
+
419
+ act(() => {
420
+ result.current.clear();
421
+ });
422
+
423
+ expect(result.current.text).toBe("");
424
+ expect(result.current.isStreaming).toBe(false);
425
+ });
426
+ });