@canonmsg/agent-sdk 0.2.0 → 0.3.0

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
@@ -39,6 +39,9 @@ No additional dependencies required — the SDK uses native `fetch` and `Readabl
39
39
  | `pollingIntervalMs` | `number` | `3000` | Polling interval in milliseconds (polling mode only) |
40
40
  | `debounceMs` | `number` | `2000` | Batching window for incoming messages per conversation |
41
41
  | `historyLimit` | `number` | `50` | Number of historical messages to fetch (max 100) |
42
+ | `sessions` | `SessionOptions` | `undefined` | Enable per-conversation session queues and persistent metadata |
43
+ | `clientType` | `AgentClientType` | `'generic'` | Agent runtime label used for Canon capability detection |
44
+ | `sessionState` | `boolean` | `false` | Publish RTDB session-state for the conversations this agent is active in |
42
45
 
43
46
  ## Delivery Modes
44
47
 
@@ -70,10 +73,47 @@ The `message` event handler receives a context object with:
70
73
  | `history` | `SDKMessage[]` | Last N messages before these new ones |
71
74
  | `conversationId` | `string` | The conversation these messages belong to |
72
75
  | `conversation` | `SDKConversation` | Full conversation metadata |
73
- | `reply` | `(text: string) => Promise<{ messageId: string }>` | Send a reply to this conversation |
76
+ | `reply` | `(text: string, options?) => Promise<{ messageId: string }>` | Convenience alias for `replyFinal` |
77
+ | `replyFinal` | `(text: string, options?) => Promise<{ messageId: string }>` | Send the durable final reply for a turn |
78
+ | `replyProgress` | `(text: string, options?) => Promise<{ messageId: string }>` | Send a durable progress update during a turn |
79
+ | `agent` | `AgentContext` | Trusted Canon agent identity and access context |
80
+ | `session` | `SessionInfo \| undefined` | Per-conversation queue/session state when sessions are enabled |
81
+ | `turn` | `TurnController \| undefined` | Live turn-state helpers for thinking/streaming/tool/waiting-input |
74
82
 
75
83
  Messages from the agent itself are automatically filtered out -- your handler only receives messages from other participants.
76
84
 
85
+ ### Turn-aware example
86
+
87
+ ```typescript
88
+ agent.on('message', async ({ messages, history, replyFinal, replyProgress, turn, session }) => {
89
+ await turn?.setThinking('Reviewing the request...');
90
+
91
+ const plan = await draftPlan(messages, history, session?.messages ?? []);
92
+ await replyProgress(`Plan: ${plan.summary}`);
93
+
94
+ await turn?.setTool('Running checks...');
95
+ const result = await runWork(plan);
96
+
97
+ if (result.needsInput) {
98
+ await turn?.setWaitingInput('I need one more detail before I continue.');
99
+ return;
100
+ }
101
+
102
+ await replyFinal(result.text);
103
+ });
104
+ ```
105
+
106
+ ### Session queues
107
+
108
+ When `sessions.enabled` is on, the SDK serializes work per conversation and exposes:
109
+
110
+ - `session.id`: the conversation/session id
111
+ - `session.messages`: accumulated session context within the configured limit
112
+ - `session.metadata`: mutable per-session state
113
+ - `session.queueDepth`: number of pending inbound batches behind the current one
114
+
115
+ This is the easiest way to build agents that need per-conversation memory or queue awareness.
116
+
77
117
  ## Agent Registration
78
118
 
79
119
  Register a new agent using the static helpers (no API key needed):
@@ -127,3 +167,14 @@ process.on('SIGINT', async () => {
127
167
  process.exit(0);
128
168
  });
129
169
  ```
170
+
171
+ ## Live turn state
172
+
173
+ While a handler runs, the SDK automatically publishes Canon turn state and clears it when the turn completes. Use the `turn` helpers when you want richer live UX:
174
+
175
+ - `setThinking(text?)`
176
+ - `setStreaming(text)`
177
+ - `setTool(text)`
178
+ - `setWaitingInput(text?)`
179
+
180
+ `setWaitingInput()` keeps the turn open in `waiting_input` and optionally sends a control message to the conversation so Canon clients can render “reply to continue” correctly.
@@ -23,8 +23,9 @@ export declare class CanonAgent {
23
23
  updateConversationName(conversationId: string, name: string): Promise<void>;
24
24
  addMember(conversationId: string, userId: string): Promise<void>;
25
25
  removeMember(conversationId: string, userId: string): Promise<void>;
26
- uploadMedia(conversationId: string, data: string, mimeType: string): Promise<{
26
+ uploadMedia(conversationId: string, data: string, mimeType: string, fileName?: string): Promise<{
27
27
  url: string;
28
+ attachment: import('@canonmsg/core').MediaAttachment;
28
29
  }>;
29
30
  stop(): Promise<void>;
30
31
  private handleMessages;
@@ -1,9 +1,17 @@
1
- import { CanonClient, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
1
+ import { CanonClient, initRTDBAuth, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from '@canonmsg/core';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { AuthManager } from './auth.js';
3
4
  import { Debouncer } from './debouncer.js';
4
5
  import { PollingManager } from './polling.js';
5
6
  import { SessionManager } from './session-manager.js';
6
7
  const AUTO_MODE_THRESHOLD = 500;
8
+ const SDK_RUNTIME_CAPABILITIES = {
9
+ supportsInterrupt: false,
10
+ supportsQueue: true,
11
+ supportsInterleave: false,
12
+ supportsRequiresAction: false,
13
+ supportsNonFinalPermanentMessages: true,
14
+ };
7
15
  export class CanonAgent {
8
16
  options;
9
17
  apiClient;
@@ -47,6 +55,7 @@ export class CanonAgent {
47
55
  if (this.running)
48
56
  return;
49
57
  this.running = true;
58
+ initRTDBAuth(this.apiClient);
50
59
  // 1. Authenticate
51
60
  const { agentId } = await this.authManager.authenticate();
52
61
  this.agentId = agentId;
@@ -79,7 +88,6 @@ export class CanonAgent {
79
88
  }
80
89
  // 3c. Initialize RTDB session state reporting (opt-in)
81
90
  if (this.options.sessionState) {
82
- initRTDBAuth(this.apiClient);
83
91
  for (const id of this.cachedConversationIds) {
84
92
  writeSessionState(id, agentId, {
85
93
  cwd: process.cwd(),
@@ -126,8 +134,8 @@ export class CanonAgent {
126
134
  async removeMember(conversationId, userId) {
127
135
  return this.apiClient.removeMember(conversationId, userId);
128
136
  }
129
- async uploadMedia(conversationId, data, mimeType) {
130
- return this.apiClient.uploadMedia(conversationId, data, mimeType);
137
+ async uploadMedia(conversationId, data, mimeType, fileName) {
138
+ return this.apiClient.uploadMedia(conversationId, data, mimeType, fileName);
131
139
  }
132
140
  async stop() {
133
141
  if (!this.running)
@@ -139,6 +147,11 @@ export class CanonAgent {
139
147
  clearSessionState(id, this.agentId).catch(() => { });
140
148
  }
141
149
  }
150
+ if (this.agentId) {
151
+ for (const id of this.cachedConversationIds) {
152
+ clearTurnState(id, this.agentId).catch(() => { });
153
+ }
154
+ }
142
155
  this.pollingManager?.stop();
143
156
  this.realtimeManager?.stop();
144
157
  this.sessionManager?.destroy();
@@ -163,6 +176,44 @@ export class CanonAgent {
163
176
  async executeHandler(conversationId, messages, session) {
164
177
  if (!this.handler)
165
178
  return;
179
+ const turnId = randomUUID();
180
+ const turnOpenedAt = Date.now();
181
+ let turnState = 'thinking';
182
+ let shouldPersistTurnState = false;
183
+ const agentId = this.agentId;
184
+ const queueDepth = () => this.sessionManager?.getQueueDepth(conversationId) ?? 0;
185
+ const writeTurn = async (state) => {
186
+ if (!agentId)
187
+ return;
188
+ turnState = state;
189
+ await writeTurnState(conversationId, agentId, {
190
+ turnId,
191
+ state,
192
+ queueDepth: queueDepth(),
193
+ currentSpeakerId: agentId,
194
+ capabilities: SDK_RUNTIME_CAPABILITIES,
195
+ openedAt: turnOpenedAt,
196
+ ...(state === 'completed' || state === 'interrupted' || state === 'idle'
197
+ ? { completedAt: { '.sv': 'timestamp' } }
198
+ : {}),
199
+ }).catch(() => { });
200
+ };
201
+ const setLiveState = async (state, text, streamingStatus) => {
202
+ await writeTurn(state);
203
+ if (streamingStatus) {
204
+ try {
205
+ await this.apiClient.setStreaming({
206
+ conversationId,
207
+ text: text ?? '',
208
+ status: streamingStatus,
209
+ messageId: turnId,
210
+ });
211
+ }
212
+ catch {
213
+ // Non-critical
214
+ }
215
+ }
216
+ };
166
217
  // Show thinking indicator and keep it alive (5s client-side expiry)
167
218
  try {
168
219
  await this.apiClient.setTyping(conversationId, true, 'thinking');
@@ -170,8 +221,17 @@ export class CanonAgent {
170
221
  catch {
171
222
  // Non-critical
172
223
  }
224
+ await setLiveState('thinking', 'Thinking...', 'thinking');
173
225
  const thinkingKeepalive = setInterval(() => {
174
226
  this.apiClient.setTyping(conversationId, true, 'thinking').catch(() => { });
227
+ if (turnState === 'thinking') {
228
+ this.apiClient.setStreaming({
229
+ conversationId,
230
+ text: 'Thinking...',
231
+ status: 'thinking',
232
+ messageId: turnId,
233
+ }).catch(() => { });
234
+ }
175
235
  }, 3500);
176
236
  try {
177
237
  // Fetch history from API
@@ -185,19 +245,44 @@ export class CanonAgent {
185
245
  const conversation = conversations.find((c) => c.id === conversationId);
186
246
  if (!conversation)
187
247
  return;
188
- // Build reply function — switch to typing, send, then clear
189
- const reply = async (text) => {
248
+ // Build reply functions
249
+ const replyFinal = async (text, options) => {
190
250
  try {
191
251
  await this.apiClient.setTyping(conversationId, true, 'typing');
192
252
  }
193
253
  catch { }
194
- const result = await this.apiClient.sendMessage(conversationId, text);
254
+ await writeTurn('completed');
255
+ const result = await this.apiClient.sendMessage(conversationId, text, {
256
+ ...(options ?? {}),
257
+ metadata: {
258
+ ...(options?.metadata ?? {}),
259
+ turnId,
260
+ turnSemantics: 'turn_complete',
261
+ turnComplete: true,
262
+ },
263
+ });
195
264
  try {
196
265
  await this.apiClient.setTyping(conversationId, false);
197
266
  }
198
267
  catch { }
268
+ try {
269
+ await this.apiClient.clearStreaming(conversationId);
270
+ }
271
+ catch { }
199
272
  return result;
200
273
  };
274
+ const replyProgress = async (text, options) => {
275
+ await setLiveState('streaming', text, 'streaming');
276
+ return this.apiClient.sendMessage(conversationId, text, {
277
+ ...(options ?? {}),
278
+ metadata: {
279
+ ...(options?.metadata ?? {}),
280
+ turnId,
281
+ turnSemantics: 'progress',
282
+ turnComplete: false,
283
+ },
284
+ });
285
+ };
201
286
  // Enrich history messages with isOwner
202
287
  if (this.agentContext?.ownerId) {
203
288
  const ownerId = this.agentContext.ownerId;
@@ -225,7 +310,9 @@ export class CanonAgent {
225
310
  history,
226
311
  conversationId,
227
312
  conversation,
228
- reply,
313
+ reply: replyFinal,
314
+ replyFinal,
315
+ replyProgress,
229
316
  deleteMessage,
230
317
  markAsRead,
231
318
  leave,
@@ -238,8 +325,45 @@ export class CanonAgent {
238
325
  id: session.id,
239
326
  messages: session.messages,
240
327
  metadata: session.metadata,
328
+ queueDepth: queueDepth(),
241
329
  }
242
330
  : undefined,
331
+ turn: {
332
+ id: turnId,
333
+ get state() {
334
+ return turnState;
335
+ },
336
+ setThinking: async (text) => {
337
+ await setLiveState('thinking', text ?? 'Thinking...', 'thinking');
338
+ },
339
+ setStreaming: async (text) => {
340
+ await setLiveState('streaming', text, 'streaming');
341
+ },
342
+ setTool: async (text) => {
343
+ await setLiveState('tool', text, 'tool');
344
+ },
345
+ setWaitingInput: async (text) => {
346
+ shouldPersistTurnState = true;
347
+ try {
348
+ await this.apiClient.clearStreaming(conversationId);
349
+ }
350
+ catch { }
351
+ await writeTurn('waiting_input');
352
+ try {
353
+ await this.apiClient.setTyping(conversationId, false);
354
+ }
355
+ catch { }
356
+ if (text) {
357
+ await this.apiClient.sendMessage(conversationId, text, {
358
+ metadata: {
359
+ turnId,
360
+ turnSemantics: 'control',
361
+ replyBehavior: 'suppress_auto_reply',
362
+ },
363
+ });
364
+ }
365
+ },
366
+ },
243
367
  });
244
368
  // Auto-mark conversation as read after successful processing
245
369
  if (this.options.autoMarkRead) {
@@ -253,6 +377,7 @@ export class CanonAgent {
253
377
  }
254
378
  catch (err) {
255
379
  console.error(`[canon-sdk] Handler error for ${conversationId}:`, err);
380
+ await writeTurn('interrupted');
256
381
  }
257
382
  finally {
258
383
  clearInterval(thinkingKeepalive);
@@ -261,6 +386,13 @@ export class CanonAgent {
261
386
  await this.apiClient.setTyping(conversationId, false);
262
387
  }
263
388
  catch { }
389
+ try {
390
+ await this.apiClient.clearStreaming(conversationId);
391
+ }
392
+ catch { }
393
+ if (agentId && !shouldPersistTurnState) {
394
+ await clearTurnState(conversationId, agentId).catch(() => { });
395
+ }
264
396
  }
265
397
  }
266
398
  // Static registration helpers (unauthenticated)
package/dist/polling.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { shouldDispatchInboundMessage } from './turn-filter.js';
1
2
  export class PollingManager {
2
3
  apiClient;
3
4
  debouncer;
@@ -38,7 +39,11 @@ export class PollingManager {
38
39
  const msgTime = new Date(m.createdAt).getTime();
39
40
  return msgTime > lastSeen && m.senderId !== this.agentId;
40
41
  });
41
- for (const msg of newMessages) {
42
+ const dispatchable = await Promise.all(newMessages.map(async (message) => ({
43
+ message,
44
+ allow: await shouldDispatchInboundMessage(convo.id, this.agentId, message),
45
+ })));
46
+ for (const msg of dispatchable.filter((entry) => entry.allow).map((entry) => entry.message)) {
42
47
  this.debouncer.add(convo.id, msg);
43
48
  }
44
49
  // Update lastSeen to latest message timestamp
package/dist/realtime.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CanonClient, CanonStream } from '@canonmsg/core';
2
+ import { shouldDispatchInboundMessage } from './turn-filter.js';
2
3
  const DISCOVERY_INTERVAL_MS = 5_000;
3
4
  /**
4
5
  * Wraps @canonmsg/core's CanonStream with SDK-specific features:
@@ -36,6 +37,7 @@ export class RealtimeManager {
36
37
  imageUrl: m.imageUrl ?? null,
37
38
  audioUrl: m.audioUrl ?? null,
38
39
  audioDurationMs: m.audioDurationMs ?? null,
40
+ attachments: m.attachments ?? [],
39
41
  mentions: m.mentions ?? [],
40
42
  replyTo: m.replyTo ?? null,
41
43
  replyToPosition: m.replyToPosition ?? null,
@@ -107,7 +109,13 @@ export class RealtimeManager {
107
109
  for (const convoId of newConvoIds) {
108
110
  try {
109
111
  const messages = await this.apiClient.getMessages(convoId, 50);
110
- const newMessages = messages.filter((m) => m.senderId !== this.agentId);
112
+ const dispatchable = await Promise.all(messages.map(async (message) => ({
113
+ message,
114
+ allow: await shouldDispatchInboundMessage(convoId, this.agentId, message),
115
+ })));
116
+ const newMessages = dispatchable
117
+ .filter((entry) => entry.allow)
118
+ .map((entry) => entry.message);
111
119
  for (const msg of newMessages) {
112
120
  this.debouncer.add(convoId, msg);
113
121
  }
@@ -55,6 +55,8 @@ export declare class SessionManager {
55
55
  private sweep;
56
56
  /** Number of active sessions */
57
57
  get sessionCount(): number;
58
+ /** Number of queued batches waiting behind the active turn for this conversation. */
59
+ getQueueDepth(conversationId: string): number;
58
60
  /** Clean up all state */
59
61
  destroy(): void;
60
62
  }
@@ -163,6 +163,10 @@ export class SessionManager {
163
163
  get sessionCount() {
164
164
  return this.sessions.size;
165
165
  }
166
+ /** Number of queued batches waiting behind the active turn for this conversation. */
167
+ getQueueDepth(conversationId) {
168
+ return this.queues.get(conversationId)?.length ?? 0;
169
+ }
166
170
  /** Clean up all state */
167
171
  destroy() {
168
172
  if (this.sweepTimer) {
@@ -0,0 +1,2 @@
1
+ import { type CanonMessage } from '@canonmsg/core';
2
+ export declare function shouldDispatchInboundMessage(conversationId: string, agentId: string, message: CanonMessage): Promise<boolean>;
@@ -0,0 +1,38 @@
1
+ import { normalizeTurnState, rtdbRead, shouldTriggerAgentTurn, } from '@canonmsg/core';
2
+ function normalizeRuntimeTurnState(value) {
3
+ const turnState = normalizeTurnState(value);
4
+ if (turnState) {
5
+ return { state: turnState.state };
6
+ }
7
+ if (!value || typeof value !== 'object')
8
+ return null;
9
+ const state = value.state;
10
+ if (state === 'running') {
11
+ return { state: 'streaming' };
12
+ }
13
+ if (state === 'requires_action') {
14
+ return { state: 'waiting_input' };
15
+ }
16
+ return null;
17
+ }
18
+ export async function shouldDispatchInboundMessage(conversationId, agentId, message) {
19
+ if (message.senderId === agentId)
20
+ return false;
21
+ let senderTurnState = null;
22
+ try {
23
+ const [turnState, sessionState] = await Promise.all([
24
+ rtdbRead(`/turn-state/${conversationId}/${message.senderId}`),
25
+ rtdbRead(`/session-state/${conversationId}/${message.senderId}`),
26
+ ]);
27
+ senderTurnState = normalizeRuntimeTurnState(turnState)
28
+ ?? normalizeRuntimeTurnState(sessionState);
29
+ }
30
+ catch {
31
+ senderTurnState = null;
32
+ }
33
+ return shouldTriggerAgentTurn({
34
+ senderType: message.senderType,
35
+ metadata: message.metadata,
36
+ senderTurnState,
37
+ }).allow;
38
+ }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AgentClientType, CanonMessage, CanonConversation, AgentContext, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
2
- import type { CanonMessage, CanonConversation } from '@canonmsg/core';
1
+ export type { AgentClientType, CanonMessage, CanonConversation, AgentContext, SendMessageOptions, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
2
+ import type { CanonMessage, CanonConversation, SendMessageOptions } from '@canonmsg/core';
3
3
  export type SDKMessage = CanonMessage;
4
4
  export type SDKConversation = CanonConversation;
5
5
  export interface SessionInfo {
@@ -9,13 +9,28 @@ export interface SessionInfo {
9
9
  messages: SDKMessage[];
10
10
  /** Arbitrary per-session state the agent can read/write across handler calls */
11
11
  metadata: Record<string, unknown>;
12
+ queueDepth?: number;
13
+ }
14
+ export interface TurnController {
15
+ id: string;
16
+ state: import('@canonmsg/core').TurnLifecycleState;
17
+ setThinking: (text?: string) => Promise<void>;
18
+ setStreaming: (text: string) => Promise<void>;
19
+ setTool: (text: string) => Promise<void>;
20
+ setWaitingInput: (text?: string) => Promise<void>;
12
21
  }
13
22
  export interface MessageHandlerContext {
14
23
  messages: SDKMessage[];
15
24
  history: SDKMessage[];
16
25
  conversationId: string;
17
26
  conversation: SDKConversation;
18
- reply: (text: string) => Promise<{
27
+ reply: (text: string, options?: SendMessageOptions) => Promise<{
28
+ messageId: string;
29
+ }>;
30
+ replyFinal: (text: string, options?: SendMessageOptions) => Promise<{
31
+ messageId: string;
32
+ }>;
33
+ replyProgress: (text: string, options?: SendMessageOptions) => Promise<{
19
34
  messageId: string;
20
35
  }>;
21
36
  /** Soft-delete a message (agent must be the sender) */
@@ -34,6 +49,8 @@ export interface MessageHandlerContext {
34
49
  agent: import('@canonmsg/core').AgentContext;
35
50
  /** Per-conversation session state. Present when sessions are enabled. */
36
51
  session?: SessionInfo;
52
+ /** Turn lifecycle helpers for live-work rendering and progress reporting. */
53
+ turn?: TurnController;
37
54
  }
38
55
  export type MessageHandler = (ctx: MessageHandlerContext) => Promise<void>;
39
56
  export interface SessionOptions {
@@ -62,10 +79,8 @@ export interface CanonAgentOptions {
62
79
  /** Agent client type for capability detection. Defaults to 'generic'. */
63
80
  clientType?: import('@canonmsg/core').AgentClientType;
64
81
  /**
65
- * Enable RTDB session state reporting. Off by default.
66
- * When enabled, the SDK writes { cwd, isActive, ... } to
67
- * /session-state/{convoId}/{agentId} so the Canon app can display
68
- * the agent's active status and configuration.
82
+ * Enable RTDB session-state reporting. Off by default.
83
+ * Turn-state reporting is automatic while handlers run.
69
84
  */
70
85
  sessionState?: boolean;
71
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,15 +15,15 @@
15
15
  "dist"
16
16
  ],
17
17
  "scripts": {
18
- "build": "tsc",
18
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
19
19
  "dev": "tsc --watch",
20
- "prepublishOnly": "npm run build"
20
+ "prepack": "npm run build"
21
21
  },
22
22
  "engines": {
23
23
  "node": ">=18.0.0"
24
24
  },
25
25
  "dependencies": {
26
- "@canonmsg/core": "^0.2.2"
26
+ "@canonmsg/core": "^0.3.0"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
@@ -1,64 +0,0 @@
1
- import { SDKMessage, SDKConversation, SendMessageOptions, CreateConversationOptions, AgentContext, VisibilityConfig, ContactRequestInfo } from './types';
2
- export declare class ApiClient {
3
- private baseUrl;
4
- private apiKey;
5
- constructor(apiKey: string, baseUrl?: string);
6
- private authHeaders;
7
- getAuthToken(): Promise<{
8
- token: string;
9
- expiresAt: string;
10
- agentId: string;
11
- }>;
12
- getAgentMe(): Promise<AgentContext>;
13
- getConversations(): Promise<SDKConversation[]>;
14
- getMessages(conversationId: string, limit?: number, before?: string): Promise<SDKMessage[]>;
15
- sendMessage(conversationId: string, text: string, options?: SendMessageOptions): Promise<{
16
- messageId: string;
17
- }>;
18
- createConversation(options: CreateConversationOptions): Promise<{
19
- conversationId: string;
20
- }>;
21
- requestContact(targetUserId: string, message?: string): Promise<{
22
- requestId: string;
23
- }>;
24
- getContactRequests(): Promise<ContactRequestInfo[]>;
25
- approveContactRequest(requestId: string): Promise<void>;
26
- rejectContactRequest(requestId: string): Promise<void>;
27
- updateVisibility(config: VisibilityConfig): Promise<void>;
28
- uploadMedia(conversationId: string, data: string, mimeType: string): Promise<{
29
- url: string;
30
- }>;
31
- updateTopic(conversationId: string, topic: string): Promise<void>;
32
- deleteMessage(conversationId: string, messageId: string): Promise<void>;
33
- markAsRead(conversationId: string): Promise<void>;
34
- leaveConversation(conversationId: string): Promise<void>;
35
- react(conversationId: string, messageId: string, emoji: string): Promise<void>;
36
- updateConversationName(conversationId: string, name: string): Promise<void>;
37
- addMember(conversationId: string, userId: string): Promise<void>;
38
- removeMember(conversationId: string, userId: string): Promise<void>;
39
- setTyping(conversationId: string, typing: boolean): Promise<void>;
40
- static register(baseUrl: string | undefined, body: {
41
- name: string;
42
- description: string;
43
- ownerPhone: string;
44
- developerInfo: string;
45
- avatarUrl?: string;
46
- }): Promise<{
47
- requestId: string;
48
- }>;
49
- static checkStatus(baseUrl: string | undefined, requestId: string): Promise<{
50
- status: string;
51
- agentName: string;
52
- agentId?: string;
53
- apiKey?: string;
54
- }>;
55
- }
56
- export declare class ApiError extends Error {
57
- status: number;
58
- constructor(status: number, body: string);
59
- }
60
- export declare class ApprovalRequiredError extends Error {
61
- targetUserId: string;
62
- hint: string;
63
- constructor(targetUserId: string, hint: string);
64
- }
@@ -1,257 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ApprovalRequiredError = exports.ApiError = exports.ApiClient = void 0;
4
- const DEFAULT_BASE_URL = 'https://api-6m6mlelskq-uc.a.run.app';
5
- class ApiClient {
6
- baseUrl;
7
- apiKey;
8
- constructor(apiKey, baseUrl) {
9
- this.apiKey = apiKey;
10
- this.baseUrl = baseUrl || DEFAULT_BASE_URL;
11
- }
12
- authHeaders() {
13
- return {
14
- 'Authorization': `Bearer ${this.apiKey}`,
15
- 'Content-Type': 'application/json',
16
- };
17
- }
18
- async getAuthToken() {
19
- const res = await fetch(`${this.baseUrl}/agents/auth-token`, {
20
- method: 'POST',
21
- headers: this.authHeaders(),
22
- });
23
- if (!res.ok)
24
- throw new ApiError(res.status, await res.text());
25
- return res.json();
26
- }
27
- async getAgentMe() {
28
- const res = await fetch(`${this.baseUrl}/agents/me`, {
29
- headers: this.authHeaders(),
30
- });
31
- if (!res.ok)
32
- throw new ApiError(res.status, await res.text());
33
- return res.json();
34
- }
35
- async getConversations() {
36
- const res = await fetch(`${this.baseUrl}/conversations`, {
37
- headers: this.authHeaders(),
38
- });
39
- if (!res.ok)
40
- throw new ApiError(res.status, await res.text());
41
- const data = await res.json();
42
- return data.conversations;
43
- }
44
- async getMessages(conversationId, limit = 50, before) {
45
- const params = new URLSearchParams({ limit: String(limit) });
46
- if (before)
47
- params.set('before', before);
48
- const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, { headers: this.authHeaders() });
49
- if (!res.ok)
50
- throw new ApiError(res.status, await res.text());
51
- const data = await res.json();
52
- return data.messages;
53
- }
54
- async sendMessage(conversationId, text, options) {
55
- const res = await fetch(`${this.baseUrl}/messages/send`, {
56
- method: 'POST',
57
- headers: this.authHeaders(),
58
- body: JSON.stringify({ conversationId, text, ...options }),
59
- });
60
- if (!res.ok)
61
- throw new ApiError(res.status, await res.text());
62
- return res.json();
63
- }
64
- async createConversation(options) {
65
- const res = await fetch(`${this.baseUrl}/conversations/create`, {
66
- method: 'POST',
67
- headers: this.authHeaders(),
68
- body: JSON.stringify(options),
69
- });
70
- if (!res.ok) {
71
- const body = await res.text();
72
- try {
73
- const parsed = JSON.parse(body);
74
- if (parsed.code === 'APPROVAL_REQUIRED') {
75
- throw new ApprovalRequiredError(options.targetUserId ?? '', parsed.hint ?? 'Contact request required');
76
- }
77
- }
78
- catch (e) {
79
- if (e instanceof ApprovalRequiredError)
80
- throw e;
81
- }
82
- throw new ApiError(res.status, body);
83
- }
84
- return res.json();
85
- }
86
- async requestContact(targetUserId, message) {
87
- const res = await fetch(`${this.baseUrl}/contacts/request`, {
88
- method: 'POST',
89
- headers: this.authHeaders(),
90
- body: JSON.stringify({ targetUserId, message }),
91
- });
92
- if (!res.ok)
93
- throw new ApiError(res.status, await res.text());
94
- return res.json();
95
- }
96
- async getContactRequests() {
97
- const res = await fetch(`${this.baseUrl}/contacts/requests`, {
98
- headers: this.authHeaders(),
99
- });
100
- if (!res.ok)
101
- throw new ApiError(res.status, await res.text());
102
- const data = await res.json();
103
- return data.requests;
104
- }
105
- async approveContactRequest(requestId) {
106
- const res = await fetch(`${this.baseUrl}/contacts/requests/${requestId}/approve`, {
107
- method: 'POST',
108
- headers: this.authHeaders(),
109
- });
110
- if (!res.ok)
111
- throw new ApiError(res.status, await res.text());
112
- }
113
- async rejectContactRequest(requestId) {
114
- const res = await fetch(`${this.baseUrl}/contacts/requests/${requestId}/reject`, {
115
- method: 'POST',
116
- headers: this.authHeaders(),
117
- });
118
- if (!res.ok)
119
- throw new ApiError(res.status, await res.text());
120
- }
121
- async updateVisibility(config) {
122
- const res = await fetch(`${this.baseUrl}/agents/visibility`, {
123
- method: 'PATCH',
124
- headers: this.authHeaders(),
125
- body: JSON.stringify(config),
126
- });
127
- if (!res.ok)
128
- throw new ApiError(res.status, await res.text());
129
- }
130
- async uploadMedia(conversationId, data, mimeType) {
131
- const res = await fetch(`${this.baseUrl}/media/upload`, {
132
- method: 'POST',
133
- headers: this.authHeaders(),
134
- body: JSON.stringify({ conversationId, mimeType, data }),
135
- });
136
- if (!res.ok)
137
- throw new ApiError(res.status, await res.text());
138
- return res.json();
139
- }
140
- async updateTopic(conversationId, topic) {
141
- const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/topic`, {
142
- method: 'PATCH',
143
- headers: this.authHeaders(),
144
- body: JSON.stringify({ topic }),
145
- });
146
- if (!res.ok)
147
- throw new ApiError(res.status, await res.text());
148
- }
149
- async deleteMessage(conversationId, messageId) {
150
- const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages/${messageId}`, {
151
- method: 'DELETE',
152
- headers: this.authHeaders(),
153
- });
154
- if (!res.ok)
155
- throw new ApiError(res.status, await res.text());
156
- }
157
- async markAsRead(conversationId) {
158
- const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/read`, {
159
- method: 'POST',
160
- headers: this.authHeaders(),
161
- });
162
- if (!res.ok)
163
- throw new ApiError(res.status, await res.text());
164
- }
165
- async leaveConversation(conversationId) {
166
- const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/leave`, {
167
- method: 'POST',
168
- headers: this.authHeaders(),
169
- });
170
- if (!res.ok)
171
- throw new ApiError(res.status, await res.text());
172
- }
173
- async react(conversationId, messageId, emoji) {
174
- const res = await fetch(`${this.baseUrl}/messages/react`, {
175
- method: 'POST',
176
- headers: this.authHeaders(),
177
- body: JSON.stringify({ conversationId, messageId, emoji }),
178
- });
179
- if (!res.ok)
180
- throw new ApiError(res.status, await res.text());
181
- }
182
- async updateConversationName(conversationId, name) {
183
- const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
184
- method: 'PATCH',
185
- headers: this.authHeaders(),
186
- body: JSON.stringify({ name }),
187
- });
188
- if (!res.ok)
189
- throw new ApiError(res.status, await res.text());
190
- }
191
- async addMember(conversationId, userId) {
192
- const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/members`, {
193
- method: 'POST',
194
- headers: this.authHeaders(),
195
- body: JSON.stringify({ userId }),
196
- });
197
- if (!res.ok)
198
- throw new ApiError(res.status, await res.text());
199
- }
200
- async removeMember(conversationId, userId) {
201
- const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/members/${userId}`, {
202
- method: 'DELETE',
203
- headers: this.authHeaders(),
204
- });
205
- if (!res.ok)
206
- throw new ApiError(res.status, await res.text());
207
- }
208
- async setTyping(conversationId, typing) {
209
- const res = await fetch(`${this.baseUrl}/typing`, {
210
- method: 'POST',
211
- headers: this.authHeaders(),
212
- body: JSON.stringify({ conversationId, typing }),
213
- });
214
- if (!res.ok)
215
- throw new ApiError(res.status, await res.text());
216
- }
217
- // Static helpers for unauthenticated registration endpoints
218
- static async register(baseUrl, body) {
219
- const url = baseUrl || DEFAULT_BASE_URL;
220
- const res = await fetch(`${url}/agents/register`, {
221
- method: 'POST',
222
- headers: { 'Content-Type': 'application/json' },
223
- body: JSON.stringify(body),
224
- });
225
- if (!res.ok)
226
- throw new ApiError(res.status, await res.text());
227
- return res.json();
228
- }
229
- static async checkStatus(baseUrl, requestId) {
230
- const url = baseUrl || DEFAULT_BASE_URL;
231
- const res = await fetch(`${url}/agents/status/${requestId}`);
232
- if (!res.ok)
233
- throw new ApiError(res.status, await res.text());
234
- return res.json();
235
- }
236
- }
237
- exports.ApiClient = ApiClient;
238
- class ApiError extends Error {
239
- status;
240
- constructor(status, body) {
241
- super(`API error ${status}: ${body}`);
242
- this.name = 'ApiError';
243
- this.status = status;
244
- }
245
- }
246
- exports.ApiError = ApiError;
247
- class ApprovalRequiredError extends Error {
248
- targetUserId;
249
- hint;
250
- constructor(targetUserId, hint) {
251
- super(`Contact request required for user ${targetUserId}`);
252
- this.name = 'ApprovalRequiredError';
253
- this.targetUserId = targetUserId;
254
- this.hint = hint;
255
- }
256
- }
257
- exports.ApprovalRequiredError = ApprovalRequiredError;