@agentick/angular 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,424 @@
1
+ /**
2
+ * AgentickService - Modern Angular service for Agentick.
3
+ *
4
+ * Uses Angular signals for reactive state management with RxJS interop.
5
+ *
6
+ * @module @agentick/angular/service
7
+ */
8
+
9
+ import {
10
+ Injectable,
11
+ InjectionToken,
12
+ type OnDestroy,
13
+ computed,
14
+ signal,
15
+ inject,
16
+ } from "@angular/core";
17
+ import { toObservable } from "@angular/core/rxjs-interop";
18
+ import { Observable, Subject, filter, takeUntil } from "rxjs";
19
+ import {
20
+ createClient,
21
+ type AgentickClient,
22
+ type ConnectionState,
23
+ type StreamEvent,
24
+ type StreamingTextState,
25
+ type SessionStreamEvent,
26
+ type SessionAccessor,
27
+ type ClientExecutionHandle,
28
+ } from "@agentick/client";
29
+ import type { AgentickConfig } from "./types";
30
+
31
+ /**
32
+ * Injection token for Agentick configuration.
33
+ */
34
+ export const TENTICKLE_CONFIG = new InjectionToken<AgentickConfig>("TENTICKLE_CONFIG");
35
+
36
+ /**
37
+ * Provides AgentickService with configuration at component level.
38
+ *
39
+ * Use this to create isolated service instances for different components,
40
+ * each with their own connection and state.
41
+ *
42
+ * @example Multiple agents in one app
43
+ * ```typescript
44
+ * // Each component gets its own AgentickService instance
45
+ *
46
+ * @Component({
47
+ * selector: 'app-support-chat',
48
+ * providers: [provideAgentick({ baseUrl: '/api/support-agent' })],
49
+ * template: `<div>{{ agentick.text() }}</div>`,
50
+ * })
51
+ * export class SupportChatComponent {
52
+ * agentick = inject(AgentickService);
53
+ * }
54
+ *
55
+ * @Component({
56
+ * selector: 'app-sales-chat',
57
+ * providers: [provideAgentick({ baseUrl: '/api/sales-agent' })],
58
+ * template: `<div>{{ agentick.text() }}</div>`,
59
+ * })
60
+ * export class SalesChatComponent {
61
+ * agentick = inject(AgentickService);
62
+ * }
63
+ * ```
64
+ *
65
+ * @param config - Configuration for this service instance
66
+ * @returns Provider array to spread into component's providers
67
+ */
68
+ export function provideAgentick(config: AgentickConfig) {
69
+ return [{ provide: TENTICKLE_CONFIG, useValue: config }, AgentickService];
70
+ }
71
+
72
+ /**
73
+ * Modern Angular service for Agentick.
74
+ *
75
+ * Uses signals for state, with RxJS observables available for compatibility.
76
+ *
77
+ * @example Standalone setup
78
+ * ```typescript
79
+ * import { AgentickService, TENTICKLE_CONFIG } from '@agentick/angular';
80
+ *
81
+ * bootstrapApplication(AppComponent, {
82
+ * providers: [
83
+ * { provide: TENTICKLE_CONFIG, useValue: { baseUrl: 'https://api.example.com' } },
84
+ * ],
85
+ * });
86
+ * ```
87
+ *
88
+ * @example Component with signals
89
+ * ```typescript
90
+ * @Component({
91
+ * template: `
92
+ * @if (agentick.isConnected()) {
93
+ * <div class="response">
94
+ * {{ agentick.text() }}
95
+ * @if (agentick.isStreaming()) {
96
+ * <span class="cursor">|</span>
97
+ * }
98
+ * </div>
99
+ * <input #input />
100
+ * <button (click)="send(input.value)">Send</button>
101
+ * } @else {
102
+ * <p>Connecting...</p>
103
+ * }
104
+ * `,
105
+ * })
106
+ * export class ChatComponent {
107
+ * agentick = inject(AgentickService);
108
+ *
109
+ * constructor() {
110
+ * this.agentick.subscribe("conv-123");
111
+ * }
112
+ *
113
+ * async send(message: string) {
114
+ * const handle = this.agentick.send(message);
115
+ * await handle.result;
116
+ * }
117
+ * }
118
+ * ```
119
+ *
120
+ * @example With RxJS (for compatibility)
121
+ * ```typescript
122
+ * @Component({
123
+ * template: `{{ text$ | async }}`,
124
+ * })
125
+ * export class LegacyComponent {
126
+ * agentick = inject(AgentickService);
127
+ * text$ = this.agentick.text$;
128
+ * }
129
+ * ```
130
+ */
131
+ @Injectable({ providedIn: "root" })
132
+ export class AgentickService implements OnDestroy {
133
+ private readonly client: AgentickClient;
134
+ private readonly destroy$ = new Subject<void>();
135
+ private currentSession?: SessionAccessor;
136
+
137
+ // ══════════════════════════════════════════════════════════════════════════
138
+ // Signals - Primary State
139
+ // ══════════════════════════════════════════════════════════════════════════
140
+
141
+ /** Current connection state */
142
+ readonly connectionState = signal<ConnectionState>("disconnected");
143
+
144
+ /** Current session ID */
145
+ readonly sessionId = signal<string | undefined>(undefined);
146
+
147
+ /** Connection error, if any */
148
+ readonly error = signal<Error | undefined>(undefined);
149
+
150
+ /** Streaming text state from the client */
151
+ readonly streamingText = signal<StreamingTextState>({ text: "", isStreaming: false });
152
+
153
+ // ══════════════════════════════════════════════════════════════════════════
154
+ // Computed Signals - Derived State
155
+ // ══════════════════════════════════════════════════════════════════════════
156
+
157
+ /** Whether currently connected */
158
+ readonly isConnected = computed(() => this.connectionState() === "connected");
159
+
160
+ /** Whether currently connecting */
161
+ readonly isConnecting = computed(() => this.connectionState() === "connecting");
162
+
163
+ /** Current streaming text */
164
+ readonly text = computed(() => this.streamingText().text);
165
+
166
+ /** Whether currently streaming */
167
+ readonly isStreaming = computed(() => this.streamingText().isStreaming);
168
+
169
+ // ══════════════════════════════════════════════════════════════════════════
170
+ // RxJS Observables - For Compatibility
171
+ // ══════════════════════════════════════════════════════════════════════════
172
+
173
+ /** Observable of connection state (for RxJS users) */
174
+ readonly connectionState$: Observable<ConnectionState>;
175
+
176
+ /** Observable of whether connected (for RxJS users) */
177
+ readonly isConnected$: Observable<boolean>;
178
+
179
+ /** Observable of streaming text state (for RxJS users) */
180
+ readonly streamingText$: Observable<StreamingTextState>;
181
+
182
+ /** Observable of just the text (for RxJS users) */
183
+ readonly text$: Observable<string>;
184
+
185
+ /** Observable of whether streaming (for RxJS users) */
186
+ readonly isStreaming$: Observable<boolean>;
187
+
188
+ /** Subject for raw stream events */
189
+ private readonly eventsSubject = new Subject<StreamEvent | SessionStreamEvent>();
190
+
191
+ /** Observable of all stream events */
192
+ readonly events$ = this.eventsSubject.asObservable();
193
+
194
+ /** Subject for execution results */
195
+ private readonly resultSubject = new Subject<{
196
+ response: string;
197
+ outputs: Record<string, unknown>;
198
+ usage: { inputTokens: number; outputTokens: number; totalTokens: number };
199
+ stopReason?: string;
200
+ }>();
201
+
202
+ /** Observable of execution results */
203
+ readonly result$ = this.resultSubject.asObservable();
204
+
205
+ // ══════════════════════════════════════════════════════════════════════════
206
+ // Constructor
207
+ // ══════════════════════════════════════════════════════════════════════════
208
+
209
+ /**
210
+ * Creates a new AgentickService.
211
+ *
212
+ * @param configOrInjected - Config passed directly (for testing) or undefined to use DI
213
+ */
214
+ constructor(configOrInjected?: AgentickConfig) {
215
+ // Support both direct config (for testing) and DI injection
216
+ let config = configOrInjected;
217
+ if (!config) {
218
+ try {
219
+ config = inject(TENTICKLE_CONFIG, { optional: true }) ?? undefined;
220
+ } catch {
221
+ // Not in injection context - config must be passed directly
222
+ }
223
+ }
224
+
225
+ if (!config) {
226
+ throw new Error("AgentickService requires TENTICKLE_CONFIG to be provided");
227
+ }
228
+
229
+ this.client = createClient(config);
230
+
231
+ // Initialize observables from signals
232
+ // Note: toObservable requires injection context, so we create them conditionally
233
+ try {
234
+ this.connectionState$ = toObservable(this.connectionState);
235
+ this.isConnected$ = toObservable(this.isConnected);
236
+ this.streamingText$ = toObservable(this.streamingText);
237
+ this.text$ = toObservable(this.text);
238
+ this.isStreaming$ = toObservable(this.isStreaming);
239
+ } catch {
240
+ // Not in injection context - create manual observables for testing
241
+ this.connectionState$ = new Observable<ConnectionState>((subscriber) => {
242
+ subscriber.next(this.connectionState());
243
+ const interval = setInterval(() => {
244
+ subscriber.next(this.connectionState());
245
+ }, 10);
246
+ return () => clearInterval(interval);
247
+ });
248
+ this.isConnected$ = new Observable<boolean>((subscriber) => {
249
+ subscriber.next(this.isConnected());
250
+ const interval = setInterval(() => {
251
+ subscriber.next(this.isConnected());
252
+ }, 10);
253
+ return () => clearInterval(interval);
254
+ });
255
+ this.streamingText$ = new Observable<StreamingTextState>((subscriber) => {
256
+ subscriber.next(this.streamingText());
257
+ const interval = setInterval(() => {
258
+ subscriber.next(this.streamingText());
259
+ }, 10);
260
+ return () => clearInterval(interval);
261
+ });
262
+ this.text$ = new Observable<string>((subscriber) => {
263
+ subscriber.next(this.text());
264
+ const interval = setInterval(() => {
265
+ subscriber.next(this.text());
266
+ }, 10);
267
+ return () => clearInterval(interval);
268
+ });
269
+ this.isStreaming$ = new Observable<boolean>((subscriber) => {
270
+ subscriber.next(this.isStreaming());
271
+ const interval = setInterval(() => {
272
+ subscriber.next(this.isStreaming());
273
+ }, 10);
274
+ return () => clearInterval(interval);
275
+ });
276
+ }
277
+
278
+ this.setupSubscriptions();
279
+ }
280
+
281
+ private setupSubscriptions(): void {
282
+ // Connection state → signal
283
+ this.client.onConnectionChange((state) => {
284
+ this.connectionState.set(state);
285
+ });
286
+
287
+ // Streaming text → signal
288
+ this.client.onStreamingText((state) => {
289
+ this.streamingText.set(state);
290
+ });
291
+
292
+ // Events → subject (for filtering)
293
+ this.client.onEvent((event) => {
294
+ this.eventsSubject.next(event);
295
+ });
296
+
297
+ // Results → subject
298
+ this.client.onEvent((event) => {
299
+ if (event.type === "result") {
300
+ this.resultSubject.next(event.result);
301
+ }
302
+ });
303
+ }
304
+
305
+ // ══════════════════════════════════════════════════════════════════════════
306
+ // Session Access
307
+ // ══════════════════════════════════════════════════════════════════════════
308
+
309
+ /**
310
+ * Get a cold session accessor.
311
+ */
312
+ session(sessionId: string): SessionAccessor {
313
+ return this.client.session(sessionId);
314
+ }
315
+
316
+ /**
317
+ * Subscribe to a session and make it the active session.
318
+ */
319
+ subscribe(sessionId: string): SessionAccessor {
320
+ const accessor = this.client.subscribe(sessionId);
321
+ this.currentSession = accessor;
322
+ this.sessionId.set(sessionId);
323
+ return accessor;
324
+ }
325
+
326
+ /**
327
+ * Unsubscribe from the active session.
328
+ */
329
+ unsubscribe(): void {
330
+ this.currentSession?.unsubscribe();
331
+ this.currentSession = undefined;
332
+ this.sessionId.set(undefined);
333
+ }
334
+
335
+ // ══════════════════════════════════════════════════════════════════════════
336
+ // Messaging
337
+ // ══════════════════════════════════════════════════════════════════════════
338
+
339
+ /**
340
+ * Send a message to the session.
341
+ */
342
+ send(input: Parameters<AgentickClient["send"]>[0]): ClientExecutionHandle {
343
+ if (this.currentSession) {
344
+ return this.currentSession.send(input as any);
345
+ }
346
+ return this.client.send(input as any);
347
+ }
348
+
349
+ /**
350
+ * Abort the current execution.
351
+ */
352
+ async abort(reason?: string): Promise<void> {
353
+ if (this.currentSession) {
354
+ await this.currentSession.abort(reason);
355
+ return;
356
+ }
357
+ const id = this.sessionId();
358
+ if (id) {
359
+ await this.client.abort(id, reason);
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Close the active session on the server.
365
+ */
366
+ async close(): Promise<void> {
367
+ if (this.currentSession) {
368
+ await this.currentSession.close();
369
+ this.currentSession = undefined;
370
+ this.sessionId.set(undefined);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Clear the accumulated streaming text.
376
+ */
377
+ clearStreamingText(): void {
378
+ this.client.clearStreamingText();
379
+ }
380
+
381
+ // ══════════════════════════════════════════════════════════════════════════
382
+ // Channels
383
+ // ══════════════════════════════════════════════════════════════════════════
384
+
385
+ /**
386
+ * Get a channel accessor for custom pub/sub.
387
+ */
388
+ channel(name: string) {
389
+ if (!this.currentSession) {
390
+ throw new Error("No active session. Call subscribe(sessionId) first.");
391
+ }
392
+ return this.currentSession.channel(name);
393
+ }
394
+
395
+ /**
396
+ * Create an Observable from a channel.
397
+ */
398
+ channel$(name: string): Observable<{ type: string; payload: unknown }> {
399
+ return new Observable<{ type: string; payload: unknown }>((subscriber) => {
400
+ const channel = this.channel(name);
401
+ const unsubscribe = channel.subscribe((payload, event) => {
402
+ subscriber.next({ type: event.type, payload });
403
+ });
404
+ return () => unsubscribe();
405
+ }).pipe(takeUntil(this.destroy$));
406
+ }
407
+
408
+ /**
409
+ * Filter events by type.
410
+ */
411
+ eventsOfType(...types: StreamEvent["type"][]): Observable<StreamEvent> {
412
+ return this.events$.pipe(filter((event) => types.includes(event.type)));
413
+ }
414
+
415
+ // ══════════════════════════════════════════════════════════════════════════
416
+ // Cleanup
417
+ // ══════════════════════════════════════════════════════════════════════════
418
+
419
+ ngOnDestroy(): void {
420
+ this.destroy$.next();
421
+ this.destroy$.complete();
422
+ this.client.destroy();
423
+ }
424
+ }
package/src/index.ts ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @agentick/angular - Modern Angular bindings for Agentick
3
+ *
4
+ * Uses Angular signals for reactive state with RxJS interop for compatibility.
5
+ *
6
+ * @example Standalone setup
7
+ * ```typescript
8
+ * import { TENTICKLE_CONFIG } from '@agentick/angular';
9
+ *
10
+ * bootstrapApplication(AppComponent, {
11
+ * providers: [
12
+ * { provide: TENTICKLE_CONFIG, useValue: { baseUrl: 'https://api.example.com' } },
13
+ * ],
14
+ * });
15
+ * ```
16
+ *
17
+ * @example Component with signals (recommended)
18
+ * ```typescript
19
+ * import { Component, inject } from '@angular/core';
20
+ * import { AgentickService } from '@agentick/angular';
21
+ *
22
+ * @Component({
23
+ * selector: 'app-chat',
24
+ * standalone: true,
25
+ * template: `
26
+ * <div class="response">
27
+ * {{ agentick.text() }}
28
+ * @if (agentick.isStreaming()) {
29
+ * <span class="cursor">|</span>
30
+ * }
31
+ * </div>
32
+ * <input #input />
33
+ * <button (click)="send(input.value); input.value = ''">Send</button>
34
+ * `,
35
+ * })
36
+ * export class ChatComponent {
37
+ * agentick = inject(AgentickService);
38
+ *
39
+ * constructor() {
40
+ * this.agentick.subscribe("conv-123");
41
+ * }
42
+ *
43
+ * async send(message: string) {
44
+ * const handle = this.agentick.send(message);
45
+ * await handle.result;
46
+ * }
47
+ * }
48
+ * ```
49
+ *
50
+ * @example With RxJS (for legacy or complex reactive flows)
51
+ * ```typescript
52
+ * @Component({
53
+ * template: `
54
+ * <div>{{ text$ | async }}</div>
55
+ * `,
56
+ * })
57
+ * export class LegacyComponent {
58
+ * agentick = inject(AgentickService);
59
+ * text$ = this.agentick.text$;
60
+ * }
61
+ * ```
62
+ *
63
+ * @example Multiple agents with separate instances
64
+ * ```typescript
65
+ * import { provideAgentick, AgentickService } from '@agentick/angular';
66
+ *
67
+ * // Each component gets its own service instance
68
+ * @Component({
69
+ * selector: 'app-support-chat',
70
+ * standalone: true,
71
+ * providers: [provideAgentick({ baseUrl: '/api/support-agent' })],
72
+ * template: `<div>{{ agentick.text() }}</div>`,
73
+ * })
74
+ * export class SupportChatComponent {
75
+ * agentick = inject(AgentickService);
76
+ * }
77
+ *
78
+ * @Component({
79
+ * selector: 'app-sales-chat',
80
+ * standalone: true,
81
+ * providers: [provideAgentick({ baseUrl: '/api/sales-agent' })],
82
+ * template: `<div>{{ agentick.text() }}</div>`,
83
+ * })
84
+ * export class SalesChatComponent {
85
+ * agentick = inject(AgentickService);
86
+ * }
87
+ * ```
88
+ *
89
+ * ## Signals (Primary API)
90
+ *
91
+ * | Signal | Type | Description |
92
+ * |--------|------|-------------|
93
+ * | `connectionState()` | `ConnectionState` | Connection state |
94
+ * | `sessionId()` | `string \| undefined` | Active session ID |
95
+ * | `streamingText()` | `StreamingTextState` | Text + isStreaming |
96
+ * | `text()` | `string` | Just the text (computed) |
97
+ * | `isStreaming()` | `boolean` | Whether streaming (computed) |
98
+ *
99
+ * ## RxJS Observables (Compatibility)
100
+ *
101
+ * | Observable | Type | Description |
102
+ * |------------|------|-------------|
103
+ * | `connectionState$` | `ConnectionState` | Connection state |
104
+ * | `isConnected$` | `boolean` | Whether connected |
105
+ * | `streamingText$` | `StreamingTextState` | Text + isStreaming |
106
+ * | `text$` | `string` | Just the text |
107
+ * | `isStreaming$` | `boolean` | Whether streaming |
108
+ * | `events$` | `StreamEvent | SessionStreamEvent` | All stream events |
109
+ * | `result$` | `Result` | Execution results |
110
+ *
111
+ * ## Methods
112
+ *
113
+ * | Method | Description |
114
+ * |--------|-------------|
115
+ * | `session(sessionId)` | Get cold accessor |
116
+ * | `subscribe(sessionId)` | Subscribe (hot) |
117
+ * | `unsubscribe()` | Unsubscribe active session |
118
+ * | `send(input)` | Send message |
119
+ * | `abort(reason?)` | Abort execution |
120
+ * | `close()` | Close active session |
121
+ * | `channel(name)` | Get channel accessor |
122
+ * | `channel$(name)` | Get channel as Observable |
123
+ * | `eventsOfType(...types)` | Filter events by type |
124
+ * | `clearStreamingText()` | Clear accumulated text |
125
+ *
126
+ * @module @agentick/angular
127
+ */
128
+
129
+ // Service, token, and provider factory
130
+ export { AgentickService, TENTICKLE_CONFIG, provideAgentick } from "./agentick.service";
131
+
132
+ // Types
133
+ export type {
134
+ AgentickConfig,
135
+ TransportConfig,
136
+ AgentickClient,
137
+ ConnectionState,
138
+ StreamEvent,
139
+ SessionStreamEvent,
140
+ ClientExecutionHandle,
141
+ StreamingTextState,
142
+ ClientTransport,
143
+ } from "./types";
144
+
145
+ // Re-export createClient for advanced usage
146
+ export { createClient } from "@agentick/client";
package/src/types.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Angular integration types for Agentick.
3
+ *
4
+ * @module @agentick/angular/types
5
+ */
6
+
7
+ import type { ClientTransport } from "@agentick/client";
8
+
9
+ // ============================================================================
10
+ // Configuration Types
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Transport configuration for AgentickService.
15
+ * Can be a built-in transport type or a custom ClientTransport instance.
16
+ */
17
+ export type TransportConfig = "sse" | "websocket" | "auto" | ClientTransport;
18
+
19
+ /**
20
+ * Configuration for AgentickService.
21
+ */
22
+ export interface AgentickConfig {
23
+ /**
24
+ * Base URL of the Agentick server.
25
+ */
26
+ baseUrl: string;
27
+
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
+ * ```typescript
37
+ * import { createSharedTransport } from '@agentick/client-multiplexer';
38
+ *
39
+ * providers: [
40
+ * {
41
+ * provide: TENTICKLE_CONFIG,
42
+ * useValue: {
43
+ * baseUrl: 'https://api.example.com',
44
+ * transport: createSharedTransport({ baseUrl: 'https://api.example.com' }),
45
+ * },
46
+ * },
47
+ * ]
48
+ * ```
49
+ */
50
+ transport?: TransportConfig;
51
+
52
+ /**
53
+ * Authentication token (adds Authorization: Bearer header).
54
+ */
55
+ token?: string;
56
+
57
+ /**
58
+ * User ID for session metadata.
59
+ */
60
+ userId?: string;
61
+
62
+ /**
63
+ * Custom headers for requests.
64
+ */
65
+ headers?: Record<string, string>;
66
+
67
+ /**
68
+ * Custom path configuration.
69
+ */
70
+ paths?: {
71
+ events?: string;
72
+ send?: string;
73
+ subscribe?: string;
74
+ abort?: string;
75
+ close?: string;
76
+ toolResponse?: string;
77
+ channel?: string;
78
+ };
79
+
80
+ /**
81
+ * Request timeout in milliseconds.
82
+ * @default 30000
83
+ */
84
+ timeout?: number;
85
+ }
86
+
87
+ // ============================================================================
88
+ // Re-exports
89
+ // ============================================================================
90
+
91
+ export type {
92
+ AgentickClient,
93
+ ConnectionState,
94
+ StreamEvent,
95
+ SessionStreamEvent,
96
+ ClientExecutionHandle,
97
+ StreamingTextState,
98
+ ClientTransport,
99
+ } from "@agentick/client";