@canonmsg/agent-sdk 0.2.0 → 0.4.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 +54 -1
- package/dist/canon-agent.d.ts +2 -1
- package/dist/canon-agent.js +144 -8
- package/dist/index.d.ts +1 -1
- package/dist/policy-history.d.ts +10 -0
- package/dist/policy-history.js +11 -0
- package/dist/polling.js +16 -2
- package/dist/realtime.js +20 -2
- package/dist/session-manager.d.ts +2 -0
- package/dist/session-manager.js +4 -0
- package/dist/turn-filter.d.ts +8 -0
- package/dist/turn-filter.js +51 -0
- package/dist/types.d.ts +36 -7
- package/package.json +4 -4
- package/dist/api-client.d.ts +0 -64
- package/dist/api-client.js +0 -257
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 }>` |
|
|
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<{ turnId: string; durable: boolean; messageId: string \| null }>` | Update the live turn progress; add `durable: true` to also persist it |
|
|
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,16 @@ 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.
|
|
181
|
+
|
|
182
|
+
`replyProgress()` is ephemeral by default: it updates the live RTDB turn preview without adding a permanent Firestore message. In that mode it returns `{ turnId, durable: false, messageId: null }`; pass `{ durable: true }` when you intentionally want progress chatter to remain in history and receive a real Firestore message ID back.
|
package/dist/canon-agent.d.ts
CHANGED
|
@@ -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;
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import { CanonClient, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
|
|
1
|
+
import { CanonClient, FINAL_MESSAGE_HANDOFF_MS, 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: false,
|
|
14
|
+
};
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
7
18
|
export class CanonAgent {
|
|
8
19
|
options;
|
|
9
20
|
apiClient;
|
|
@@ -47,6 +58,7 @@ export class CanonAgent {
|
|
|
47
58
|
if (this.running)
|
|
48
59
|
return;
|
|
49
60
|
this.running = true;
|
|
61
|
+
initRTDBAuth(this.apiClient);
|
|
50
62
|
// 1. Authenticate
|
|
51
63
|
const { agentId } = await this.authManager.authenticate();
|
|
52
64
|
this.agentId = agentId;
|
|
@@ -79,7 +91,6 @@ export class CanonAgent {
|
|
|
79
91
|
}
|
|
80
92
|
// 3c. Initialize RTDB session state reporting (opt-in)
|
|
81
93
|
if (this.options.sessionState) {
|
|
82
|
-
initRTDBAuth(this.apiClient);
|
|
83
94
|
for (const id of this.cachedConversationIds) {
|
|
84
95
|
writeSessionState(id, agentId, {
|
|
85
96
|
cwd: process.cwd(),
|
|
@@ -126,8 +137,8 @@ export class CanonAgent {
|
|
|
126
137
|
async removeMember(conversationId, userId) {
|
|
127
138
|
return this.apiClient.removeMember(conversationId, userId);
|
|
128
139
|
}
|
|
129
|
-
async uploadMedia(conversationId, data, mimeType) {
|
|
130
|
-
return this.apiClient.uploadMedia(conversationId, data, mimeType);
|
|
140
|
+
async uploadMedia(conversationId, data, mimeType, fileName) {
|
|
141
|
+
return this.apiClient.uploadMedia(conversationId, data, mimeType, fileName);
|
|
131
142
|
}
|
|
132
143
|
async stop() {
|
|
133
144
|
if (!this.running)
|
|
@@ -139,6 +150,11 @@ export class CanonAgent {
|
|
|
139
150
|
clearSessionState(id, this.agentId).catch(() => { });
|
|
140
151
|
}
|
|
141
152
|
}
|
|
153
|
+
if (this.agentId) {
|
|
154
|
+
for (const id of this.cachedConversationIds) {
|
|
155
|
+
clearTurnState(id, this.agentId).catch(() => { });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
142
158
|
this.pollingManager?.stop();
|
|
143
159
|
this.realtimeManager?.stop();
|
|
144
160
|
this.sessionManager?.destroy();
|
|
@@ -163,6 +179,44 @@ export class CanonAgent {
|
|
|
163
179
|
async executeHandler(conversationId, messages, session) {
|
|
164
180
|
if (!this.handler)
|
|
165
181
|
return;
|
|
182
|
+
const turnId = randomUUID();
|
|
183
|
+
const turnOpenedAt = Date.now();
|
|
184
|
+
let turnState = 'thinking';
|
|
185
|
+
let shouldPersistTurnState = false;
|
|
186
|
+
const agentId = this.agentId;
|
|
187
|
+
const queueDepth = () => this.sessionManager?.getQueueDepth(conversationId) ?? 0;
|
|
188
|
+
const writeTurn = async (state) => {
|
|
189
|
+
if (!agentId)
|
|
190
|
+
return;
|
|
191
|
+
turnState = state;
|
|
192
|
+
await writeTurnState(conversationId, agentId, {
|
|
193
|
+
turnId,
|
|
194
|
+
state,
|
|
195
|
+
queueDepth: queueDepth(),
|
|
196
|
+
currentSpeakerId: agentId,
|
|
197
|
+
capabilities: SDK_RUNTIME_CAPABILITIES,
|
|
198
|
+
openedAt: turnOpenedAt,
|
|
199
|
+
...(state === 'completed' || state === 'interrupted' || state === 'idle'
|
|
200
|
+
? { completedAt: { '.sv': 'timestamp' } }
|
|
201
|
+
: {}),
|
|
202
|
+
}).catch(() => { });
|
|
203
|
+
};
|
|
204
|
+
const setLiveState = async (state, text, streamingStatus) => {
|
|
205
|
+
await writeTurn(state);
|
|
206
|
+
if (streamingStatus) {
|
|
207
|
+
try {
|
|
208
|
+
await this.apiClient.setStreaming({
|
|
209
|
+
conversationId,
|
|
210
|
+
text: text ?? '',
|
|
211
|
+
status: streamingStatus,
|
|
212
|
+
messageId: turnId,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Non-critical
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
166
220
|
// Show thinking indicator and keep it alive (5s client-side expiry)
|
|
167
221
|
try {
|
|
168
222
|
await this.apiClient.setTyping(conversationId, true, 'thinking');
|
|
@@ -170,8 +224,17 @@ export class CanonAgent {
|
|
|
170
224
|
catch {
|
|
171
225
|
// Non-critical
|
|
172
226
|
}
|
|
227
|
+
await setLiveState('thinking', 'Thinking...', 'thinking');
|
|
173
228
|
const thinkingKeepalive = setInterval(() => {
|
|
174
229
|
this.apiClient.setTyping(conversationId, true, 'thinking').catch(() => { });
|
|
230
|
+
if (turnState === 'thinking') {
|
|
231
|
+
this.apiClient.setStreaming({
|
|
232
|
+
conversationId,
|
|
233
|
+
text: 'Thinking...',
|
|
234
|
+
status: 'thinking',
|
|
235
|
+
messageId: turnId,
|
|
236
|
+
}).catch(() => { });
|
|
237
|
+
}
|
|
175
238
|
}, 3500);
|
|
176
239
|
try {
|
|
177
240
|
// Fetch history from API
|
|
@@ -185,19 +248,45 @@ export class CanonAgent {
|
|
|
185
248
|
const conversation = conversations.find((c) => c.id === conversationId);
|
|
186
249
|
if (!conversation)
|
|
187
250
|
return;
|
|
188
|
-
// Build reply
|
|
189
|
-
const
|
|
251
|
+
// Build reply functions
|
|
252
|
+
const replyFinal = async (text, options) => {
|
|
190
253
|
try {
|
|
191
254
|
await this.apiClient.setTyping(conversationId, true, 'typing');
|
|
192
255
|
}
|
|
193
256
|
catch { }
|
|
194
|
-
const result = await this.apiClient.sendMessage(conversationId, text
|
|
257
|
+
const result = await this.apiClient.sendMessage(conversationId, text, {
|
|
258
|
+
...(options ?? {}),
|
|
259
|
+
metadata: {
|
|
260
|
+
...(options?.metadata ?? {}),
|
|
261
|
+
turnId,
|
|
262
|
+
turnSemantics: 'turn_complete',
|
|
263
|
+
turnComplete: true,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
await sleep(FINAL_MESSAGE_HANDOFF_MS);
|
|
195
267
|
try {
|
|
196
268
|
await this.apiClient.setTyping(conversationId, false);
|
|
197
269
|
}
|
|
198
270
|
catch { }
|
|
199
271
|
return result;
|
|
200
272
|
};
|
|
273
|
+
const replyProgress = async (text, options) => {
|
|
274
|
+
await setLiveState('streaming', text, 'streaming');
|
|
275
|
+
if (!options?.durable) {
|
|
276
|
+
return { turnId, durable: false, messageId: null };
|
|
277
|
+
}
|
|
278
|
+
const { durable: _durable, ...sendOptions } = options;
|
|
279
|
+
const result = await this.apiClient.sendMessage(conversationId, text, {
|
|
280
|
+
...sendOptions,
|
|
281
|
+
metadata: {
|
|
282
|
+
...(sendOptions.metadata ?? {}),
|
|
283
|
+
turnId,
|
|
284
|
+
turnSemantics: 'progress',
|
|
285
|
+
turnComplete: false,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
return { turnId, durable: true, messageId: result.messageId };
|
|
289
|
+
};
|
|
201
290
|
// Enrich history messages with isOwner
|
|
202
291
|
if (this.agentContext?.ownerId) {
|
|
203
292
|
const ownerId = this.agentContext.ownerId;
|
|
@@ -225,7 +314,9 @@ export class CanonAgent {
|
|
|
225
314
|
history,
|
|
226
315
|
conversationId,
|
|
227
316
|
conversation,
|
|
228
|
-
reply,
|
|
317
|
+
reply: replyFinal,
|
|
318
|
+
replyFinal,
|
|
319
|
+
replyProgress,
|
|
229
320
|
deleteMessage,
|
|
230
321
|
markAsRead,
|
|
231
322
|
leave,
|
|
@@ -238,8 +329,45 @@ export class CanonAgent {
|
|
|
238
329
|
id: session.id,
|
|
239
330
|
messages: session.messages,
|
|
240
331
|
metadata: session.metadata,
|
|
332
|
+
queueDepth: queueDepth(),
|
|
241
333
|
}
|
|
242
334
|
: undefined,
|
|
335
|
+
turn: {
|
|
336
|
+
id: turnId,
|
|
337
|
+
get state() {
|
|
338
|
+
return turnState;
|
|
339
|
+
},
|
|
340
|
+
setThinking: async (text) => {
|
|
341
|
+
await setLiveState('thinking', text ?? 'Thinking...', 'thinking');
|
|
342
|
+
},
|
|
343
|
+
setStreaming: async (text) => {
|
|
344
|
+
await setLiveState('streaming', text, 'streaming');
|
|
345
|
+
},
|
|
346
|
+
setTool: async (text) => {
|
|
347
|
+
await setLiveState('tool', text, 'tool');
|
|
348
|
+
},
|
|
349
|
+
setWaitingInput: async (text) => {
|
|
350
|
+
shouldPersistTurnState = true;
|
|
351
|
+
try {
|
|
352
|
+
await this.apiClient.clearStreaming(conversationId);
|
|
353
|
+
}
|
|
354
|
+
catch { }
|
|
355
|
+
await writeTurn('waiting_input');
|
|
356
|
+
try {
|
|
357
|
+
await this.apiClient.setTyping(conversationId, false);
|
|
358
|
+
}
|
|
359
|
+
catch { }
|
|
360
|
+
if (text) {
|
|
361
|
+
await this.apiClient.sendMessage(conversationId, text, {
|
|
362
|
+
metadata: {
|
|
363
|
+
turnId,
|
|
364
|
+
turnSemantics: 'control',
|
|
365
|
+
replyBehavior: 'suppress_auto_reply',
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
},
|
|
243
371
|
});
|
|
244
372
|
// Auto-mark conversation as read after successful processing
|
|
245
373
|
if (this.options.autoMarkRead) {
|
|
@@ -253,6 +381,7 @@ export class CanonAgent {
|
|
|
253
381
|
}
|
|
254
382
|
catch (err) {
|
|
255
383
|
console.error(`[canon-sdk] Handler error for ${conversationId}:`, err);
|
|
384
|
+
await writeTurn('interrupted');
|
|
256
385
|
}
|
|
257
386
|
finally {
|
|
258
387
|
clearInterval(thinkingKeepalive);
|
|
@@ -261,6 +390,13 @@ export class CanonAgent {
|
|
|
261
390
|
await this.apiClient.setTyping(conversationId, false);
|
|
262
391
|
}
|
|
263
392
|
catch { }
|
|
393
|
+
try {
|
|
394
|
+
await this.apiClient.clearStreaming(conversationId);
|
|
395
|
+
}
|
|
396
|
+
catch { }
|
|
397
|
+
if (agentId && !shouldPersistTurnState) {
|
|
398
|
+
await clearTurnState(conversationId, agentId).catch(() => { });
|
|
399
|
+
}
|
|
264
400
|
}
|
|
265
401
|
}
|
|
266
402
|
// Static registration helpers (unauthenticated)
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { CanonApiError } from '@canonmsg/core';
|
|
|
3
3
|
export { SessionManager } from './session-manager.js';
|
|
4
4
|
export type { SessionConfig, Session } from './session-manager.js';
|
|
5
5
|
export type { AgentContext, CanonMessage, CanonConversation, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
|
|
6
|
-
export type { SDKMessage, SDKConversation, CanonAgentOptions, MessageHandler, MessageHandlerContext, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
|
6
|
+
export type { SDKMessage, SDKConversation, CanonAgentOptions, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CanonMessage, type ParticipationHistorySnapshot } from '@canonmsg/core';
|
|
2
|
+
export type { ParticipationHistorySnapshot } from '@canonmsg/core';
|
|
3
|
+
/**
|
|
4
|
+
* Builds message-specific participation history snapshots for backlog delivery.
|
|
5
|
+
*
|
|
6
|
+
* `messages` must be ordered newest-first, matching Canon's `getMessages()`
|
|
7
|
+
* API. Each snapshot is computed from older history only, never from the
|
|
8
|
+
* target message itself or newer messages that had not occurred yet.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildParticipationHistorySnapshots(messages: CanonMessage[], agentId: string): Map<string, ParticipationHistorySnapshot>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { buildParticipationHistorySnapshots as buildSharedParticipationHistorySnapshots, } from '@canonmsg/core';
|
|
2
|
+
/**
|
|
3
|
+
* Builds message-specific participation history snapshots for backlog delivery.
|
|
4
|
+
*
|
|
5
|
+
* `messages` must be ordered newest-first, matching Canon's `getMessages()`
|
|
6
|
+
* API. Each snapshot is computed from older history only, never from the
|
|
7
|
+
* target message itself or newer messages that had not occurred yet.
|
|
8
|
+
*/
|
|
9
|
+
export function buildParticipationHistorySnapshots(messages, agentId) {
|
|
10
|
+
return buildSharedParticipationHistorySnapshots(messages, agentId);
|
|
11
|
+
}
|
package/dist/polling.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildParticipationHistorySnapshots } from './policy-history.js';
|
|
2
|
+
import { shouldDispatchInboundMessage } from './turn-filter.js';
|
|
1
3
|
export class PollingManager {
|
|
2
4
|
apiClient;
|
|
3
5
|
debouncer;
|
|
@@ -31,14 +33,26 @@ export class PollingManager {
|
|
|
31
33
|
const activeConvos = this.findActiveConversations(conversations);
|
|
32
34
|
await Promise.all(activeConvos.map(async (convo) => {
|
|
33
35
|
try {
|
|
34
|
-
const
|
|
36
|
+
const page = await this.apiClient.getMessagesPage(convo.id, 50);
|
|
37
|
+
const messages = page.messages;
|
|
38
|
+
const participationHistory = buildParticipationHistorySnapshots(messages, this.agentId);
|
|
35
39
|
// Filter to only new messages (after lastSeen, not from self)
|
|
36
40
|
const lastSeen = this.lastSeenTimestamps.get(convo.id) || 0;
|
|
37
41
|
const newMessages = messages.filter((m) => {
|
|
38
42
|
const msgTime = new Date(m.createdAt).getTime();
|
|
39
43
|
return msgTime > lastSeen && m.senderId !== this.agentId;
|
|
40
44
|
});
|
|
41
|
-
|
|
45
|
+
const dispatchable = await Promise.all(newMessages.map(async (message) => ({
|
|
46
|
+
message,
|
|
47
|
+
allow: await shouldDispatchInboundMessage(convo.id, this.agentId, message, {
|
|
48
|
+
conversationType: convo.type,
|
|
49
|
+
behavior: page.behavior,
|
|
50
|
+
recentHumanCount: participationHistory.get(message.id)?.recentHumanCount,
|
|
51
|
+
consecutiveAgentTurns: participationHistory.get(message.id)?.consecutiveAgentTurns,
|
|
52
|
+
currentAgentStreakStartedByHuman: participationHistory.get(message.id)?.currentAgentStreakStartedByHuman,
|
|
53
|
+
}),
|
|
54
|
+
})));
|
|
55
|
+
for (const msg of dispatchable.filter((entry) => entry.allow).map((entry) => entry.message)) {
|
|
42
56
|
this.debouncer.add(convo.id, msg);
|
|
43
57
|
}
|
|
44
58
|
// Update lastSeen to latest message timestamp
|
package/dist/realtime.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { CanonClient, CanonStream } from '@canonmsg/core';
|
|
2
|
+
import { buildParticipationHistorySnapshots } from './policy-history.js';
|
|
3
|
+
import { shouldDispatchInboundMessage } from './turn-filter.js';
|
|
2
4
|
const DISCOVERY_INTERVAL_MS = 5_000;
|
|
3
5
|
/**
|
|
4
6
|
* Wraps @canonmsg/core's CanonStream with SDK-specific features:
|
|
@@ -36,6 +38,7 @@ export class RealtimeManager {
|
|
|
36
38
|
imageUrl: m.imageUrl ?? null,
|
|
37
39
|
audioUrl: m.audioUrl ?? null,
|
|
38
40
|
audioDurationMs: m.audioDurationMs ?? null,
|
|
41
|
+
attachments: m.attachments ?? [],
|
|
39
42
|
mentions: m.mentions ?? [],
|
|
40
43
|
replyTo: m.replyTo ?? null,
|
|
41
44
|
replyToPosition: m.replyToPosition ?? null,
|
|
@@ -106,8 +109,23 @@ export class RealtimeManager {
|
|
|
106
109
|
// Fetch and deliver pending messages from new conversations
|
|
107
110
|
for (const convoId of newConvoIds) {
|
|
108
111
|
try {
|
|
109
|
-
const
|
|
110
|
-
const
|
|
112
|
+
const conversation = convos.find((item) => item.id === convoId);
|
|
113
|
+
const page = await this.apiClient.getMessagesPage(convoId, 50);
|
|
114
|
+
const messages = page.messages;
|
|
115
|
+
const participationHistory = buildParticipationHistorySnapshots(messages, this.agentId);
|
|
116
|
+
const dispatchable = await Promise.all(messages.map(async (message) => ({
|
|
117
|
+
message,
|
|
118
|
+
allow: await shouldDispatchInboundMessage(convoId, this.agentId, message, {
|
|
119
|
+
conversationType: conversation?.type ?? 'unknown',
|
|
120
|
+
behavior: page.behavior,
|
|
121
|
+
recentHumanCount: participationHistory.get(message.id)?.recentHumanCount,
|
|
122
|
+
consecutiveAgentTurns: participationHistory.get(message.id)?.consecutiveAgentTurns,
|
|
123
|
+
currentAgentStreakStartedByHuman: participationHistory.get(message.id)?.currentAgentStreakStartedByHuman,
|
|
124
|
+
}),
|
|
125
|
+
})));
|
|
126
|
+
const newMessages = dispatchable
|
|
127
|
+
.filter((entry) => entry.allow)
|
|
128
|
+
.map((entry) => entry.message);
|
|
111
129
|
for (const msg of newMessages) {
|
|
112
130
|
this.debouncer.add(convoId, msg);
|
|
113
131
|
}
|
|
@@ -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
|
}
|
package/dist/session-manager.js
CHANGED
|
@@ -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,8 @@
|
|
|
1
|
+
import { type ResolvedAgentBehaviorPolicy, type CanonMessage } from '@canonmsg/core';
|
|
2
|
+
export declare function shouldDispatchInboundMessage(conversationId: string, agentId: string, message: CanonMessage, options?: {
|
|
3
|
+
conversationType?: 'direct' | 'group' | 'unknown';
|
|
4
|
+
behavior?: ResolvedAgentBehaviorPolicy | null;
|
|
5
|
+
recentHumanCount?: number;
|
|
6
|
+
consecutiveAgentTurns?: number;
|
|
7
|
+
currentAgentStreakStartedByHuman?: boolean;
|
|
8
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { evaluateParticipationPolicy, 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, options) {
|
|
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
|
+
const triggerDecision = shouldTriggerAgentTurn({
|
|
34
|
+
senderType: message.senderType,
|
|
35
|
+
metadata: message.metadata,
|
|
36
|
+
senderTurnState,
|
|
37
|
+
});
|
|
38
|
+
if (!triggerDecision.allow)
|
|
39
|
+
return false;
|
|
40
|
+
if (!options?.behavior)
|
|
41
|
+
return true;
|
|
42
|
+
return evaluateParticipationPolicy(options.behavior, {
|
|
43
|
+
conversationType: options.conversationType ?? 'unknown',
|
|
44
|
+
senderType: message.senderType,
|
|
45
|
+
isOwner: message.isOwner,
|
|
46
|
+
mentionedAgent: Array.isArray(message.mentions) && message.mentions.includes(agentId),
|
|
47
|
+
recentHumanCount: options.recentHumanCount,
|
|
48
|
+
consecutiveAgentTurns: options.consecutiveAgentTurns,
|
|
49
|
+
currentAgentStreakStartedByHuman: options.currentAgentStreakStartedByHuman,
|
|
50
|
+
}).allow;
|
|
51
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
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
|
+
export interface ProgressMessageOptions extends SendMessageOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Persist the progress update to Firestore.
|
|
8
|
+
* By default, progress stays ephemeral and only updates the live RTDB turn state.
|
|
9
|
+
*/
|
|
10
|
+
durable?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type ProgressMessageResult = {
|
|
13
|
+
turnId: string;
|
|
14
|
+
durable: false;
|
|
15
|
+
messageId: null;
|
|
16
|
+
} | {
|
|
17
|
+
turnId: string;
|
|
18
|
+
durable: true;
|
|
19
|
+
messageId: string;
|
|
20
|
+
};
|
|
5
21
|
export interface SessionInfo {
|
|
6
22
|
/** Session ID (= conversationId) */
|
|
7
23
|
id: string;
|
|
@@ -9,15 +25,28 @@ export interface SessionInfo {
|
|
|
9
25
|
messages: SDKMessage[];
|
|
10
26
|
/** Arbitrary per-session state the agent can read/write across handler calls */
|
|
11
27
|
metadata: Record<string, unknown>;
|
|
28
|
+
queueDepth?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface TurnController {
|
|
31
|
+
id: string;
|
|
32
|
+
state: import('@canonmsg/core').TurnLifecycleState;
|
|
33
|
+
setThinking: (text?: string) => Promise<void>;
|
|
34
|
+
setStreaming: (text: string) => Promise<void>;
|
|
35
|
+
setTool: (text: string) => Promise<void>;
|
|
36
|
+
setWaitingInput: (text?: string) => Promise<void>;
|
|
12
37
|
}
|
|
13
38
|
export interface MessageHandlerContext {
|
|
14
39
|
messages: SDKMessage[];
|
|
15
40
|
history: SDKMessage[];
|
|
16
41
|
conversationId: string;
|
|
17
42
|
conversation: SDKConversation;
|
|
18
|
-
reply: (text: string) => Promise<{
|
|
43
|
+
reply: (text: string, options?: SendMessageOptions) => Promise<{
|
|
44
|
+
messageId: string;
|
|
45
|
+
}>;
|
|
46
|
+
replyFinal: (text: string, options?: SendMessageOptions) => Promise<{
|
|
19
47
|
messageId: string;
|
|
20
48
|
}>;
|
|
49
|
+
replyProgress: (text: string, options?: ProgressMessageOptions) => Promise<ProgressMessageResult>;
|
|
21
50
|
/** Soft-delete a message (agent must be the sender) */
|
|
22
51
|
deleteMessage: (messageId: string) => Promise<void>;
|
|
23
52
|
/** Mark conversation as read */
|
|
@@ -34,6 +63,8 @@ export interface MessageHandlerContext {
|
|
|
34
63
|
agent: import('@canonmsg/core').AgentContext;
|
|
35
64
|
/** Per-conversation session state. Present when sessions are enabled. */
|
|
36
65
|
session?: SessionInfo;
|
|
66
|
+
/** Turn lifecycle helpers for live-work rendering and progress reporting. */
|
|
67
|
+
turn?: TurnController;
|
|
37
68
|
}
|
|
38
69
|
export type MessageHandler = (ctx: MessageHandlerContext) => Promise<void>;
|
|
39
70
|
export interface SessionOptions {
|
|
@@ -62,10 +93,8 @@ export interface CanonAgentOptions {
|
|
|
62
93
|
/** Agent client type for capability detection. Defaults to 'generic'. */
|
|
63
94
|
clientType?: import('@canonmsg/core').AgentClientType;
|
|
64
95
|
/**
|
|
65
|
-
* Enable RTDB session
|
|
66
|
-
*
|
|
67
|
-
* /session-state/{convoId}/{agentId} so the Canon app can display
|
|
68
|
-
* the agent's active status and configuration.
|
|
96
|
+
* Enable RTDB session-state reporting. Off by default.
|
|
97
|
+
* Turn-state reporting is automatic while handlers run.
|
|
69
98
|
*/
|
|
70
99
|
sessionState?: boolean;
|
|
71
100
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
-
"
|
|
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.
|
|
26
|
+
"@canonmsg/core": "^0.4.0"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
package/dist/api-client.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/api-client.js
DELETED
|
@@ -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;
|