@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.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/context.d.ts +90 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +120 -0
- package/dist/context.js.map +1 -0
- package/dist/hooks.d.ts +290 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +442 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/__tests__/hooks.spec.tsx +426 -0
- package/src/context.tsx +133 -0
- package/src/hooks.ts +612 -0
- package/src/index.ts +123 -0
- package/src/types.ts +271 -0
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks for Agentick.
|
|
3
|
+
*
|
|
4
|
+
* @module @agentick/react/hooks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
useState,
|
|
9
|
+
useEffect,
|
|
10
|
+
useCallback,
|
|
11
|
+
useRef,
|
|
12
|
+
useSyncExternalStore,
|
|
13
|
+
useMemo,
|
|
14
|
+
useContext,
|
|
15
|
+
} from "react";
|
|
16
|
+
import type {
|
|
17
|
+
AgentickClient,
|
|
18
|
+
ConnectionState,
|
|
19
|
+
StreamEvent,
|
|
20
|
+
SessionStreamEvent,
|
|
21
|
+
StreamingTextState,
|
|
22
|
+
SessionAccessor,
|
|
23
|
+
} from "@agentick/client";
|
|
24
|
+
import { AgentickContext } from "./context";
|
|
25
|
+
import type {
|
|
26
|
+
UseSessionOptions,
|
|
27
|
+
UseSessionResult,
|
|
28
|
+
UseEventsOptions,
|
|
29
|
+
UseEventsResult,
|
|
30
|
+
UseStreamingTextOptions,
|
|
31
|
+
UseStreamingTextResult,
|
|
32
|
+
UseConnectionOptions,
|
|
33
|
+
UseConnectionResult,
|
|
34
|
+
} from "./types";
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// useClient
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Access the Agentick client from context.
|
|
42
|
+
*
|
|
43
|
+
* @throws If used outside of AgentickProvider
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* import { useClient } from '@agentick/react';
|
|
48
|
+
*
|
|
49
|
+
* function MyComponent() {
|
|
50
|
+
* const client = useClient();
|
|
51
|
+
*
|
|
52
|
+
* // Direct client access for advanced use cases
|
|
53
|
+
* const handleCustomChannel = () => {
|
|
54
|
+
* const session = client.session('conv-123');
|
|
55
|
+
* const channel = session.channel('custom');
|
|
56
|
+
* channel.publish('event', { data: 'value' });
|
|
57
|
+
* };
|
|
58
|
+
*
|
|
59
|
+
* return <button onClick={handleCustomChannel}>Send</button>;
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function useClient(): AgentickClient {
|
|
64
|
+
const context = useContext(AgentickContext);
|
|
65
|
+
|
|
66
|
+
if (!context) {
|
|
67
|
+
throw new Error("useClient must be used within a AgentickProvider");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return context.client;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// useConnectionState (alias for useConnection)
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Subscribe to connection state changes.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* import { useConnectionState } from '@agentick/react';
|
|
83
|
+
*
|
|
84
|
+
* function ConnectionIndicator() {
|
|
85
|
+
* const state = useConnectionState();
|
|
86
|
+
*
|
|
87
|
+
* return (
|
|
88
|
+
* <div className={`indicator ${state}`}>
|
|
89
|
+
* {state === 'connected' ? 'Online' : 'Offline'}
|
|
90
|
+
* </div>
|
|
91
|
+
* );
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function useConnectionState(): ConnectionState {
|
|
96
|
+
const client = useClient();
|
|
97
|
+
const [state, setState] = useState<ConnectionState>(client.state);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
// Sync initial state
|
|
101
|
+
setState(client.state);
|
|
102
|
+
|
|
103
|
+
// Subscribe to changes
|
|
104
|
+
const unsubscribe = client.onConnectionChange(setState);
|
|
105
|
+
return unsubscribe;
|
|
106
|
+
}, [client]);
|
|
107
|
+
|
|
108
|
+
return state;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// useConnection
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read the SSE connection state.
|
|
117
|
+
*/
|
|
118
|
+
export function useConnection(_options: UseConnectionOptions = {}): UseConnectionResult {
|
|
119
|
+
const client = useClient();
|
|
120
|
+
const [state, setState] = useState<ConnectionState>(client.state);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
setState(client.state);
|
|
124
|
+
return client.onConnectionChange(setState);
|
|
125
|
+
}, [client]);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
state,
|
|
129
|
+
isConnected: state === "connected",
|
|
130
|
+
isConnecting: state === "connecting",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// useSession
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Work with a specific session.
|
|
140
|
+
*
|
|
141
|
+
* @example Basic usage with session ID
|
|
142
|
+
* ```tsx
|
|
143
|
+
* import { useSession } from '@agentick/react';
|
|
144
|
+
*
|
|
145
|
+
* function Chat({ sessionId }: { sessionId: string }) {
|
|
146
|
+
* const { send, isSubscribed, subscribe } = useSession({ sessionId });
|
|
147
|
+
* const [input, setInput] = useState('');
|
|
148
|
+
*
|
|
149
|
+
* // Subscribe on mount
|
|
150
|
+
* useEffect(() => {
|
|
151
|
+
* subscribe();
|
|
152
|
+
* }, [subscribe]);
|
|
153
|
+
*
|
|
154
|
+
* const handleSend = async () => {
|
|
155
|
+
* await send(input);
|
|
156
|
+
* setInput('');
|
|
157
|
+
* };
|
|
158
|
+
*
|
|
159
|
+
* return (
|
|
160
|
+
* <div>
|
|
161
|
+
* <input value={input} onChange={(e) => setInput(e.target.value)} />
|
|
162
|
+
* <button onClick={handleSend}>Send</button>
|
|
163
|
+
* </div>
|
|
164
|
+
* );
|
|
165
|
+
* }
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* @example Ephemeral session (no sessionId)
|
|
169
|
+
* ```tsx
|
|
170
|
+
* function QuickChat() {
|
|
171
|
+
* const { send } = useSession();
|
|
172
|
+
*
|
|
173
|
+
* // Each send creates/uses an ephemeral session
|
|
174
|
+
* const handleSend = () => send('Hello!');
|
|
175
|
+
*
|
|
176
|
+
* return <button onClick={handleSend}>Ask</button>;
|
|
177
|
+
* }
|
|
178
|
+
* ```
|
|
179
|
+
*
|
|
180
|
+
* @example Auto-subscribe
|
|
181
|
+
* ```tsx
|
|
182
|
+
* function Chat({ sessionId }: { sessionId: string }) {
|
|
183
|
+
* const { send, isSubscribed } = useSession({
|
|
184
|
+
* sessionId,
|
|
185
|
+
* autoSubscribe: true,
|
|
186
|
+
* });
|
|
187
|
+
*
|
|
188
|
+
* if (!isSubscribed) return <div>Subscribing...</div>;
|
|
189
|
+
*
|
|
190
|
+
* return <ChatInterface />;
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function useSession(options: UseSessionOptions = {}): UseSessionResult {
|
|
195
|
+
const { sessionId, autoSubscribe = false } = options;
|
|
196
|
+
|
|
197
|
+
const client = useClient();
|
|
198
|
+
const mountedRef = useRef(true);
|
|
199
|
+
|
|
200
|
+
// Get or create session accessor
|
|
201
|
+
const accessor = useMemo<SessionAccessor | undefined>(() => {
|
|
202
|
+
if (!sessionId) return undefined;
|
|
203
|
+
return client.session(sessionId);
|
|
204
|
+
}, [client, sessionId]);
|
|
205
|
+
|
|
206
|
+
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
207
|
+
|
|
208
|
+
// Cleanup on unmount
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
mountedRef.current = true;
|
|
211
|
+
return () => {
|
|
212
|
+
mountedRef.current = false;
|
|
213
|
+
};
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
// Subscribe function
|
|
217
|
+
const subscribe = useCallback(() => {
|
|
218
|
+
if (!accessor) return;
|
|
219
|
+
accessor.subscribe();
|
|
220
|
+
if (mountedRef.current) {
|
|
221
|
+
setIsSubscribed(true);
|
|
222
|
+
}
|
|
223
|
+
}, [accessor]);
|
|
224
|
+
|
|
225
|
+
// Unsubscribe function
|
|
226
|
+
const unsubscribe = useCallback(() => {
|
|
227
|
+
if (!accessor) return;
|
|
228
|
+
accessor.unsubscribe();
|
|
229
|
+
if (mountedRef.current) {
|
|
230
|
+
setIsSubscribed(false);
|
|
231
|
+
}
|
|
232
|
+
}, [accessor]);
|
|
233
|
+
|
|
234
|
+
// Auto-subscribe
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (autoSubscribe && accessor && !isSubscribed) {
|
|
237
|
+
subscribe();
|
|
238
|
+
}
|
|
239
|
+
}, [autoSubscribe, accessor, isSubscribed, subscribe]);
|
|
240
|
+
|
|
241
|
+
// Send function
|
|
242
|
+
const send = useCallback(
|
|
243
|
+
(input: Parameters<UseSessionResult["send"]>[0]) => {
|
|
244
|
+
if (accessor) {
|
|
245
|
+
const normalizedInput =
|
|
246
|
+
typeof input === "string"
|
|
247
|
+
? {
|
|
248
|
+
message: {
|
|
249
|
+
role: "user" as const,
|
|
250
|
+
content: [{ type: "text" as const, text: input }],
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
: input;
|
|
254
|
+
return accessor.send(normalizedInput as any);
|
|
255
|
+
}
|
|
256
|
+
return client.send(input as any);
|
|
257
|
+
},
|
|
258
|
+
[client, accessor],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Abort function
|
|
262
|
+
const abort = useCallback(
|
|
263
|
+
async (reason?: string) => {
|
|
264
|
+
if (accessor) {
|
|
265
|
+
await accessor.abort(reason);
|
|
266
|
+
} else if (sessionId) {
|
|
267
|
+
await client.abort(sessionId, reason);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
[client, accessor, sessionId],
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Close function
|
|
274
|
+
const close = useCallback(async () => {
|
|
275
|
+
if (accessor) {
|
|
276
|
+
await accessor.close();
|
|
277
|
+
} else if (sessionId) {
|
|
278
|
+
await client.closeSession(sessionId);
|
|
279
|
+
}
|
|
280
|
+
}, [client, accessor, sessionId]);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
sessionId,
|
|
284
|
+
isSubscribed,
|
|
285
|
+
subscribe,
|
|
286
|
+
unsubscribe,
|
|
287
|
+
send,
|
|
288
|
+
abort,
|
|
289
|
+
close,
|
|
290
|
+
accessor,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ============================================================================
|
|
295
|
+
// useEvents
|
|
296
|
+
// ============================================================================
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Subscribe to stream events.
|
|
300
|
+
*
|
|
301
|
+
* Returns the latest event (not accumulated). Use useStreamingText
|
|
302
|
+
* for accumulated text from content_delta events.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```tsx
|
|
306
|
+
* import { useEvents } from '@agentick/react';
|
|
307
|
+
*
|
|
308
|
+
* function EventLog() {
|
|
309
|
+
* const { event } = useEvents();
|
|
310
|
+
*
|
|
311
|
+
* useEffect(() => {
|
|
312
|
+
* if (event) {
|
|
313
|
+
* console.log('Event:', event.type, event);
|
|
314
|
+
* }
|
|
315
|
+
* }, [event]);
|
|
316
|
+
*
|
|
317
|
+
* return <div>Latest: {event?.type}</div>;
|
|
318
|
+
* }
|
|
319
|
+
* ```
|
|
320
|
+
*
|
|
321
|
+
* @example With filter
|
|
322
|
+
* ```tsx
|
|
323
|
+
* function ToolCalls() {
|
|
324
|
+
* const { event } = useEvents({ filter: ['tool_call', 'tool_result'] });
|
|
325
|
+
*
|
|
326
|
+
* if (!event) return null;
|
|
327
|
+
*
|
|
328
|
+
* return <div>Tool: {event.type === 'tool_call' ? event.name : 'result'}</div>;
|
|
329
|
+
* }
|
|
330
|
+
* ```
|
|
331
|
+
*
|
|
332
|
+
* @example Session-specific events
|
|
333
|
+
* ```tsx
|
|
334
|
+
* function SessionEvents({ sessionId }: { sessionId: string }) {
|
|
335
|
+
* const { event } = useEvents({ sessionId });
|
|
336
|
+
* // Only receives events for this session
|
|
337
|
+
* return <div>{event?.type}</div>;
|
|
338
|
+
* }
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
export function useEvents(options: UseEventsOptions = {}): UseEventsResult {
|
|
342
|
+
const { filter, sessionId, enabled = true } = options;
|
|
343
|
+
|
|
344
|
+
const client = useClient();
|
|
345
|
+
const [event, setEvent] = useState<StreamEvent | SessionStreamEvent | undefined>();
|
|
346
|
+
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
if (!enabled) return;
|
|
349
|
+
|
|
350
|
+
// Use session-specific subscription if sessionId provided
|
|
351
|
+
if (sessionId) {
|
|
352
|
+
const accessor = client.session(sessionId);
|
|
353
|
+
const unsubscribe = accessor.onEvent((incoming) => {
|
|
354
|
+
if (filter && !filter.includes(incoming.type)) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
setEvent(incoming);
|
|
358
|
+
});
|
|
359
|
+
return unsubscribe;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Global subscription
|
|
363
|
+
const unsubscribe = client.onEvent((incoming) => {
|
|
364
|
+
if (filter && !filter.includes(incoming.type)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
setEvent(incoming);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return unsubscribe;
|
|
371
|
+
}, [client, sessionId, enabled, filter]);
|
|
372
|
+
|
|
373
|
+
const clear = useCallback(() => {
|
|
374
|
+
setEvent(undefined);
|
|
375
|
+
}, []);
|
|
376
|
+
|
|
377
|
+
return { event, clear };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// useStreamingText
|
|
382
|
+
// ============================================================================
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Subscribe to streaming text from the client.
|
|
386
|
+
*
|
|
387
|
+
* Uses the client's built-in streaming text accumulation which handles
|
|
388
|
+
* tick_start, content_delta, tick_end, and execution_end events.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```tsx
|
|
392
|
+
* import { useStreamingText } from '@agentick/react';
|
|
393
|
+
*
|
|
394
|
+
* function StreamingResponse() {
|
|
395
|
+
* const { text, isStreaming } = useStreamingText();
|
|
396
|
+
*
|
|
397
|
+
* return (
|
|
398
|
+
* <div>
|
|
399
|
+
* <p>{text}</p>
|
|
400
|
+
* {isStreaming && <span className="cursor">|</span>}
|
|
401
|
+
* </div>
|
|
402
|
+
* );
|
|
403
|
+
* }
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
export function useStreamingText(options: UseStreamingTextOptions = {}): UseStreamingTextResult {
|
|
407
|
+
const { enabled = true } = options;
|
|
408
|
+
const client = useClient();
|
|
409
|
+
|
|
410
|
+
// Use useSyncExternalStore for concurrent-safe subscription
|
|
411
|
+
const state = useSyncExternalStore<StreamingTextState>(
|
|
412
|
+
useCallback(
|
|
413
|
+
(onStoreChange) => {
|
|
414
|
+
if (!enabled) return () => {};
|
|
415
|
+
return client.onStreamingText(onStoreChange);
|
|
416
|
+
},
|
|
417
|
+
[client, enabled],
|
|
418
|
+
),
|
|
419
|
+
() => (enabled ? client.streamingText : { text: "", isStreaming: false }),
|
|
420
|
+
() => ({ text: "", isStreaming: false }),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const clear = useCallback(() => {
|
|
424
|
+
client.clearStreamingText();
|
|
425
|
+
}, [client]);
|
|
426
|
+
|
|
427
|
+
return { text: state.text, isStreaming: state.isStreaming, clear };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ============================================================================
|
|
431
|
+
// useContextInfo
|
|
432
|
+
// ============================================================================
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Context utilization info from the server.
|
|
436
|
+
* Updated after each tick with token usage and model capabilities.
|
|
437
|
+
*/
|
|
438
|
+
export interface ContextInfo {
|
|
439
|
+
/** Model ID (e.g., "gpt-4o", "claude-3-5-sonnet-20241022") */
|
|
440
|
+
modelId: string;
|
|
441
|
+
/** Human-readable model name */
|
|
442
|
+
modelName?: string;
|
|
443
|
+
/** Provider name (e.g., "openai", "anthropic") */
|
|
444
|
+
provider?: string;
|
|
445
|
+
/** Context window size in tokens */
|
|
446
|
+
contextWindow?: number;
|
|
447
|
+
/** Input tokens used this tick */
|
|
448
|
+
inputTokens: number;
|
|
449
|
+
/** Output tokens generated this tick */
|
|
450
|
+
outputTokens: number;
|
|
451
|
+
/** Total tokens this tick */
|
|
452
|
+
totalTokens: number;
|
|
453
|
+
/** Context utilization percentage (0-100) */
|
|
454
|
+
utilization?: number;
|
|
455
|
+
/** Max output tokens for this model */
|
|
456
|
+
maxOutputTokens?: number;
|
|
457
|
+
/** Model capabilities */
|
|
458
|
+
supportsVision?: boolean;
|
|
459
|
+
supportsToolUse?: boolean;
|
|
460
|
+
isReasoningModel?: boolean;
|
|
461
|
+
/** Current tick number */
|
|
462
|
+
tick: number;
|
|
463
|
+
/** Cumulative usage across all ticks in this execution */
|
|
464
|
+
cumulativeUsage?: {
|
|
465
|
+
inputTokens: number;
|
|
466
|
+
outputTokens: number;
|
|
467
|
+
totalTokens: number;
|
|
468
|
+
ticks: number;
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Options for useContextInfo hook.
|
|
474
|
+
*/
|
|
475
|
+
export interface UseContextInfoOptions {
|
|
476
|
+
/**
|
|
477
|
+
* Optional session ID to filter events for.
|
|
478
|
+
* If not provided, receives context info from any session.
|
|
479
|
+
*/
|
|
480
|
+
sessionId?: string;
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Whether the hook is enabled.
|
|
484
|
+
* If false, no context info subscription is created.
|
|
485
|
+
* @default true
|
|
486
|
+
*/
|
|
487
|
+
enabled?: boolean;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Return value from useContextInfo hook.
|
|
492
|
+
*/
|
|
493
|
+
export interface UseContextInfoResult {
|
|
494
|
+
/**
|
|
495
|
+
* Latest context info (null before first tick completes).
|
|
496
|
+
*/
|
|
497
|
+
contextInfo: ContextInfo | null;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Clear the current context info.
|
|
501
|
+
*/
|
|
502
|
+
clear: () => void;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Subscribe to context utilization info from the server.
|
|
507
|
+
*
|
|
508
|
+
* Receives context_update events after each tick with:
|
|
509
|
+
* - Token usage (input, output, total)
|
|
510
|
+
* - Context utilization percentage
|
|
511
|
+
* - Model capabilities (vision, tools, reasoning)
|
|
512
|
+
* - Cumulative usage across ticks
|
|
513
|
+
*
|
|
514
|
+
* @example Basic usage
|
|
515
|
+
* ```tsx
|
|
516
|
+
* import { useContextInfo } from '@agentick/react';
|
|
517
|
+
*
|
|
518
|
+
* function ContextBar() {
|
|
519
|
+
* const { contextInfo } = useContextInfo();
|
|
520
|
+
*
|
|
521
|
+
* if (!contextInfo) return null;
|
|
522
|
+
*
|
|
523
|
+
* return (
|
|
524
|
+
* <div className="context-bar">
|
|
525
|
+
* <span>{contextInfo.modelId}</span>
|
|
526
|
+
* <span>{contextInfo.utilization?.toFixed(1)}% used</span>
|
|
527
|
+
* <progress value={contextInfo.utilization} max={100} />
|
|
528
|
+
* </div>
|
|
529
|
+
* );
|
|
530
|
+
* }
|
|
531
|
+
* ```
|
|
532
|
+
*
|
|
533
|
+
* @example Session-specific context info
|
|
534
|
+
* ```tsx
|
|
535
|
+
* function SessionContext({ sessionId }: { sessionId: string }) {
|
|
536
|
+
* const { contextInfo } = useContextInfo({ sessionId });
|
|
537
|
+
*
|
|
538
|
+
* if (!contextInfo) return <span>No context yet</span>;
|
|
539
|
+
*
|
|
540
|
+
* return (
|
|
541
|
+
* <span>
|
|
542
|
+
* {contextInfo.inputTokens.toLocaleString()} /
|
|
543
|
+
* {contextInfo.contextWindow?.toLocaleString() ?? '?'} tokens
|
|
544
|
+
* </span>
|
|
545
|
+
* );
|
|
546
|
+
* }
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
export function useContextInfo(options: UseContextInfoOptions = {}): UseContextInfoResult {
|
|
550
|
+
const { sessionId, enabled = true } = options;
|
|
551
|
+
const client = useClient();
|
|
552
|
+
const [contextInfo, setContextInfo] = useState<ContextInfo | null>(null);
|
|
553
|
+
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
if (!enabled) return;
|
|
556
|
+
|
|
557
|
+
// Subscribe to context_update events
|
|
558
|
+
const handleEvent = (event: StreamEvent | SessionStreamEvent) => {
|
|
559
|
+
if (event.type !== "context_update") return;
|
|
560
|
+
|
|
561
|
+
// Type assertion since we filtered by type
|
|
562
|
+
const ctxEvent = event as StreamEvent & {
|
|
563
|
+
modelId: string;
|
|
564
|
+
modelName?: string;
|
|
565
|
+
provider?: string;
|
|
566
|
+
contextWindow?: number;
|
|
567
|
+
inputTokens: number;
|
|
568
|
+
outputTokens: number;
|
|
569
|
+
totalTokens: number;
|
|
570
|
+
utilization?: number;
|
|
571
|
+
maxOutputTokens?: number;
|
|
572
|
+
supportsVision?: boolean;
|
|
573
|
+
supportsToolUse?: boolean;
|
|
574
|
+
isReasoningModel?: boolean;
|
|
575
|
+
tick: number;
|
|
576
|
+
cumulativeUsage?: ContextInfo["cumulativeUsage"];
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
setContextInfo({
|
|
580
|
+
modelId: ctxEvent.modelId,
|
|
581
|
+
modelName: ctxEvent.modelName,
|
|
582
|
+
provider: ctxEvent.provider,
|
|
583
|
+
contextWindow: ctxEvent.contextWindow,
|
|
584
|
+
inputTokens: ctxEvent.inputTokens,
|
|
585
|
+
outputTokens: ctxEvent.outputTokens,
|
|
586
|
+
totalTokens: ctxEvent.totalTokens,
|
|
587
|
+
utilization: ctxEvent.utilization,
|
|
588
|
+
maxOutputTokens: ctxEvent.maxOutputTokens,
|
|
589
|
+
supportsVision: ctxEvent.supportsVision,
|
|
590
|
+
supportsToolUse: ctxEvent.supportsToolUse,
|
|
591
|
+
isReasoningModel: ctxEvent.isReasoningModel,
|
|
592
|
+
tick: ctxEvent.tick,
|
|
593
|
+
cumulativeUsage: ctxEvent.cumulativeUsage,
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// Use session-specific subscription if sessionId provided
|
|
598
|
+
if (sessionId) {
|
|
599
|
+
const accessor = client.session(sessionId);
|
|
600
|
+
return accessor.onEvent(handleEvent);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Global subscription
|
|
604
|
+
return client.onEvent(handleEvent);
|
|
605
|
+
}, [client, sessionId, enabled]);
|
|
606
|
+
|
|
607
|
+
const clear = useCallback(() => {
|
|
608
|
+
setContextInfo(null);
|
|
609
|
+
}, []);
|
|
610
|
+
|
|
611
|
+
return { contextInfo, clear };
|
|
612
|
+
}
|