@canonmsg/agent-sdk 7.1.1 → 7.1.2
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 +14 -2
- package/dist/canon-agent.js +78 -12
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +21 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -169,8 +169,8 @@ The `message` event handler receives a context object with:
|
|
|
169
169
|
| `conversationId` | `string` | The conversation these messages belong to |
|
|
170
170
|
| `conversation` | `CanonConversation` | Full conversation metadata |
|
|
171
171
|
| `groupContext` | `CanonGroupContext \| undefined` | Lightweight group awareness for group conversations |
|
|
172
|
-
| `replyFinal` | `(text: string, options?) => Promise<{ messageId: string }>` | Send the durable final reply for a turn |
|
|
173
|
-
| `replyProgress` | `(text: string, options?) => Promise<{ turnId: string; durable: boolean; messageId: string \| null }>` | Update the live turn progress; add `durable: true` to also persist it |
|
|
172
|
+
| `replyFinal` | `(text: string, options?) => Promise<{ messageId: string; messageIds: string[] }>` | Send the durable final reply for a turn |
|
|
173
|
+
| `replyProgress` | `(text: string, options?) => Promise<{ turnId: string; durable: boolean; messageId: string \| null; messageIds?: string[] }>` | Update the live turn progress; add `durable: true` to also persist it |
|
|
174
174
|
| `deleteMessage` | `(messageId: string) => Promise<void>` | Soft-delete a message sent by this agent |
|
|
175
175
|
| `markAsRead` | `() => Promise<void>` | Advance this agent's read cursor for the conversation |
|
|
176
176
|
| `leave` | `() => Promise<void>` | Leave the current group conversation |
|
|
@@ -395,3 +395,15 @@ While a handler runs, the SDK automatically publishes Canon turn state and clear
|
|
|
395
395
|
`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.
|
|
396
396
|
|
|
397
397
|
`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.
|
|
398
|
+
|
|
399
|
+
### Long text
|
|
400
|
+
|
|
401
|
+
Canon caps a single message at 4 KB of UTF-8 text, and rejects anything longer outright. Two send paths split oversized text for you instead of failing:
|
|
402
|
+
|
|
403
|
+
- `replyFinal()` — delivered as ordered parts. Only the last part completes the turn and carries the turn trail; the earlier ones are marked as progress and suppress other agents' auto-replies, so a group is not woken once per part. Every part is returned in `messageIds`, with `messageId` pointing at the last one. A reply that fits is a single message with an untouched id and metadata.
|
|
404
|
+
- `replyProgress(text, { durable: true })` — split into ordered progress messages, which Canon clients still fold into the turn trail once the turn ends, exactly like a short update. `messageIds` lists them all.
|
|
405
|
+
|
|
406
|
+
Every other send path passes your text through as-is, so text over the cap still fails there. Notably:
|
|
407
|
+
|
|
408
|
+
- `media.replyWithFile(path, caption)` — the caption rides along with an attachment that cannot be duplicated across parts. Keep captions short and send long prose as a separate `replyFinal()`.
|
|
409
|
+
- `sendContextualMessage()` and `reachOut({ text })` — a different endpoint with no chunked sender behind it.
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection,
|
|
1
|
+
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { AuthManager } from './auth.js';
|
|
4
4
|
import { Debouncer } from './debouncer.js';
|
|
@@ -12,6 +12,8 @@ const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
|
|
|
12
12
|
const DEFAULT_RUNTIME_INPUT_TIMEOUT_MS = 5 * 60_000;
|
|
13
13
|
const RUNTIME_INPUT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,160}$/;
|
|
14
14
|
const SDK_MESSAGE_ID_READABLE_MAX = 120;
|
|
15
|
+
/** Canon's message id ceiling, matching core's chunked sender. */
|
|
16
|
+
const CANON_MESSAGE_ID_MAX = 160;
|
|
15
17
|
const SDK_RUNTIME_CAPABILITIES = {
|
|
16
18
|
supportsInterrupt: false,
|
|
17
19
|
supportsInputInterrupt: false,
|
|
@@ -170,6 +172,18 @@ function safeRuntimeCardId(value) {
|
|
|
170
172
|
function normalizeResponseUserId(value) {
|
|
171
173
|
return value?.trim() || undefined;
|
|
172
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Part id for text this sdk split itself, mirroring the `-part-N` rule (and the
|
|
177
|
+
* 160-character id cap) that core's chunked sender applies to a split final, so
|
|
178
|
+
* a caller-supplied id yields the same shape whichever path the text took.
|
|
179
|
+
* Undefined base id in, undefined out: the server then names the parts.
|
|
180
|
+
*/
|
|
181
|
+
function chunkPartMessageId(baseMessageId, part) {
|
|
182
|
+
if (!baseMessageId)
|
|
183
|
+
return undefined;
|
|
184
|
+
const suffix = `-part-${part}`;
|
|
185
|
+
return `${baseMessageId.slice(0, Math.max(0, CANON_MESSAGE_ID_MAX - suffix.length))}${suffix}`;
|
|
186
|
+
}
|
|
173
187
|
function buildSdkMessageId(parts) {
|
|
174
188
|
const raw = parts
|
|
175
189
|
.map((part) => part == null ? '' : String(part))
|
|
@@ -1371,15 +1385,40 @@ export class CanonAgent {
|
|
|
1371
1385
|
throwIfAborted();
|
|
1372
1386
|
const { durable: _durable, ...sendOptions } = options;
|
|
1373
1387
|
const sendOptionsWithContext = withActiveSelfContext(sendOptions);
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1388
|
+
// Progress text is caller-controlled too, so it can also blow past
|
|
1389
|
+
// Canon's 4 KB cap. It is split here rather than through core's chunked
|
|
1390
|
+
// sender on purpose: chunk parts carry a `messageChunk` marker, and
|
|
1391
|
+
// Canon clients read a progress message that has one as durable answer
|
|
1392
|
+
// content (chat-domain's `isEphemeralProgressMessage`). Routing progress
|
|
1393
|
+
// through the chunked sender would therefore leave an oversized status
|
|
1394
|
+
// update in the timeline forever, while the same update under the cap is
|
|
1395
|
+
// folded into the turn trail once the turn ends. Plain progress parts
|
|
1396
|
+
// keep long and short updates behaving identically, and each part stays
|
|
1397
|
+
// a single unretried send, exactly as one progress update always was.
|
|
1398
|
+
const parts = splitTextByUtf8Bytes(text);
|
|
1399
|
+
const messageIds = [];
|
|
1400
|
+
for (const [index, part] of parts.entries()) {
|
|
1401
|
+
throwIfAborted();
|
|
1402
|
+
const partMessageId = parts.length > 1
|
|
1403
|
+
? chunkPartMessageId(sendOptionsWithContext.messageId, index + 1)
|
|
1404
|
+
: sendOptionsWithContext.messageId;
|
|
1405
|
+
const result = await this.apiClient.sendMessage(conversationId, part, {
|
|
1406
|
+
...sendOptionsWithContext,
|
|
1407
|
+
...(partMessageId !== undefined ? { messageId: partMessageId } : {}),
|
|
1408
|
+
metadata: {
|
|
1409
|
+
...(sendOptionsWithContext.metadata ?? {}),
|
|
1410
|
+
turnId,
|
|
1411
|
+
turnSemantics: 'progress',
|
|
1412
|
+
},
|
|
1413
|
+
});
|
|
1414
|
+
messageIds.push(result.messageId);
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
turnId,
|
|
1418
|
+
durable: true,
|
|
1419
|
+
messageId: messageIds[messageIds.length - 1],
|
|
1420
|
+
messageIds,
|
|
1421
|
+
};
|
|
1383
1422
|
};
|
|
1384
1423
|
// Enrich history messages with isOwner
|
|
1385
1424
|
if (this.agentContext?.ownerId) {
|
|
@@ -1407,15 +1446,42 @@ export class CanonAgent {
|
|
|
1407
1446
|
return base;
|
|
1408
1447
|
return activeSelfContextId ? { ...base, selfContextId: activeSelfContextId } : base;
|
|
1409
1448
|
};
|
|
1410
|
-
|
|
1449
|
+
// Core's chunked sender walks the parts in a plain loop and knows nothing
|
|
1450
|
+
// about this turn's abort signal, so a stop landing after part 1 would
|
|
1451
|
+
// otherwise let the rest of the answer post anyway — including the last
|
|
1452
|
+
// part, which completes the turn. Sending every part through a client that
|
|
1453
|
+
// re-checks the signal first is the sdk-side guard for that; the `sleep`
|
|
1454
|
+
// override below only covers a retry backoff.
|
|
1455
|
+
const abortAwareClient = Object.create(this.apiClient, {
|
|
1456
|
+
sendMessage: {
|
|
1457
|
+
value: (targetConversationId, targetText, targetOptions) => {
|
|
1458
|
+
throwIfAborted();
|
|
1459
|
+
return this.apiClient.sendMessage(targetConversationId, targetText, targetOptions);
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
});
|
|
1463
|
+
// Canon rejects any message whose text is over 4 KB of UTF-8 with a 400,
|
|
1464
|
+
// which no retry can fix — an oversized reply used to throw out of the
|
|
1465
|
+
// handler with nothing durable written, so a long answer just vanished.
|
|
1466
|
+
// Core's chunked sender splits such a reply into ordered `-part-N`
|
|
1467
|
+
// messages instead, completing the turn on the last part only. A reply
|
|
1468
|
+
// that FITS still goes out as a single message under the plain id with
|
|
1469
|
+
// untouched metadata, so the common case (and the interim→final handoff
|
|
1470
|
+
// that keys on that id) is unchanged.
|
|
1471
|
+
const sendDurableMessage = async (text, options, fallbackMessageIdParts) => {
|
|
1411
1472
|
const messageId = options?.messageId
|
|
1412
1473
|
?? buildSdkMessageId([...fallbackMessageIdParts, durableMessageSequence += 1]);
|
|
1413
|
-
|
|
1474
|
+
const { messageIds } = await sendMessageWithRetryChunked(abortAwareClient, conversationId, text, {
|
|
1414
1475
|
...(options ?? {}),
|
|
1415
1476
|
messageId,
|
|
1416
1477
|
}, {
|
|
1417
1478
|
sleep: (ms) => sleepWithAbort(ms, abortController.signal),
|
|
1418
1479
|
});
|
|
1480
|
+
// `messageId` stays the single id callers expect. When the text was
|
|
1481
|
+
// chunked it points at the LAST part — the message that carries
|
|
1482
|
+
// turn_complete and the turn trail, so replies and reactions thread off
|
|
1483
|
+
// the end of the answer, not its head. `messageIds` has the full set.
|
|
1484
|
+
return { messageId: messageIds[messageIds.length - 1], messageIds };
|
|
1419
1485
|
};
|
|
1420
1486
|
// Build agent context (fallback to minimal if not yet received)
|
|
1421
1487
|
const agent = this.agentContext ?? {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, infe
|
|
|
7
7
|
export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, MaterializedCanonReplyContext, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
|
8
8
|
export type { SessionConfig, Session } from './session-manager.js';
|
|
9
9
|
export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonReplyContext, CanonSelfContext, CanonTurnContextV2, CanonRuntimeDescriptor, MessageUpdatedPayload, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, CreateConversationResult, DirectSessionSelection, } from '@canonmsg/core';
|
|
10
|
-
export type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
|
10
|
+
export type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, FinalMessageResult, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,19 @@ export interface ProgressMessageOptions extends SendMessageOptions {
|
|
|
8
8
|
*/
|
|
9
9
|
durable?: boolean;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Result of a durable reply.
|
|
13
|
+
*
|
|
14
|
+
* Text over Canon's 4 KB (UTF-8) message limit is delivered as ordered chunks
|
|
15
|
+
* rather than refused, so one `replyFinal` can land as more than one message.
|
|
16
|
+
* `messageId` is the last one — the message carrying `turn_complete` and the
|
|
17
|
+
* turn trail — and `messageIds` lists every part in order (a single entry when
|
|
18
|
+
* the text fit, which is the common case).
|
|
19
|
+
*/
|
|
20
|
+
export interface FinalMessageResult {
|
|
21
|
+
messageId: string;
|
|
22
|
+
messageIds: string[];
|
|
23
|
+
}
|
|
11
24
|
export type ProgressMessageResult = {
|
|
12
25
|
turnId: string;
|
|
13
26
|
durable: false;
|
|
@@ -16,6 +29,13 @@ export type ProgressMessageResult = {
|
|
|
16
29
|
turnId: string;
|
|
17
30
|
durable: true;
|
|
18
31
|
messageId: string;
|
|
32
|
+
/**
|
|
33
|
+
* Every message the update landed as, in order. A durable update over
|
|
34
|
+
* Canon's 4 KB (UTF-8) limit is split into ordered parts rather than
|
|
35
|
+
* refused; `messageId` is the last of them. One entry when the text fit,
|
|
36
|
+
* which is the common case.
|
|
37
|
+
*/
|
|
38
|
+
messageIds: string[];
|
|
19
39
|
};
|
|
20
40
|
export interface SessionInfo {
|
|
21
41
|
/** Session ID (= conversationId) */
|
|
@@ -119,9 +139,7 @@ export interface MessageHandlerContext {
|
|
|
119
139
|
conversation: CanonConversation;
|
|
120
140
|
/** Lightweight group awareness, present for group conversations. */
|
|
121
141
|
groupContext?: CanonGroupContext;
|
|
122
|
-
replyFinal: (text: string, options?: SendMessageOptions) => Promise<
|
|
123
|
-
messageId: string;
|
|
124
|
-
}>;
|
|
142
|
+
replyFinal: (text: string, options?: SendMessageOptions) => Promise<FinalMessageResult>;
|
|
125
143
|
replyProgress: (text: string, options?: ProgressMessageOptions) => Promise<ProgressMessageResult>;
|
|
126
144
|
/** Soft-delete a message (agent must be the sender) */
|
|
127
145
|
deleteMessage: (messageId: string) => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.2",
|
|
4
4
|
"description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@canonmsg/core": "^8.
|
|
31
|
+
"@canonmsg/core": "^8.1.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|