@canonmsg/agent-sdk 3.2.4 → 3.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.
@@ -1,5 +1,5 @@
1
- import { ApprovalManager, 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, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, } from '@canonmsg/core';
2
- import { randomUUID } from 'node:crypto';
1
+ import { ApprovalManager, 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, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, sendMessageWithRetry, } from '@canonmsg/core';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { AuthManager } from './auth.js';
4
4
  import { Debouncer } from './debouncer.js';
5
5
  import { buildRuntimeCardCreateArgs } from './runtime-card.js';
@@ -12,6 +12,7 @@ const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
12
12
  const DEFAULT_RUNTIME_INPUT_TIMEOUT_MS = 5 * 60_000;
13
13
  const RUNTIME_INPUT_POLL_MS = 1_000;
14
14
  const RUNTIME_INPUT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,160}$/;
15
+ const SDK_MESSAGE_ID_READABLE_MAX = 120;
15
16
  const SDK_RUNTIME_CAPABILITIES = {
16
17
  supportsInterrupt: false,
17
18
  supportsInputInterrupt: false,
@@ -32,8 +33,8 @@ const DEFAULT_SDK_RUNTIME_DESCRIPTOR = {
32
33
  responder: 'agent_owner',
33
34
  result: 'action_or_values',
34
35
  maxTimeoutMs: 30 * 60_000,
35
- blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions'],
36
- actionFieldTypes: ['text', 'textarea', 'select', 'multiSelect', 'boolean'],
36
+ blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions', 'mediaPreview', 'details'],
37
+ actionFieldTypes: ['text', 'textarea', 'select', 'multiSelect', 'boolean', 'date', 'number', 'currency', 'searchSelect', 'lineItems'],
37
38
  native: true,
38
39
  },
39
40
  },
@@ -167,6 +168,19 @@ function safeRuntimeCardId(value) {
167
168
  ? normalized
168
169
  : `card_${randomUUID()}`;
169
170
  }
171
+ function buildSdkMessageId(parts) {
172
+ const raw = parts
173
+ .map((part) => part == null ? '' : String(part))
174
+ .filter(Boolean)
175
+ .join(':');
176
+ const hash = createHash('sha256').update(raw || 'sdk-message').digest('hex').slice(0, 16);
177
+ const readable = (raw || 'sdk-message')
178
+ .replace(/[^A-Za-z0-9_.:-]+/g, '_')
179
+ .replace(/_+/g, '_')
180
+ .replace(/^[:_.-]+|[:_.-]+$/g, '')
181
+ .slice(0, SDK_MESSAGE_ID_READABLE_MAX);
182
+ return `${readable || 'sdk-message'}:${hash}`;
183
+ }
170
184
  function isRuntimePrimitiveId(value) {
171
185
  return value === 'runtime.status'
172
186
  || value === 'runtime.reasoning.set'
@@ -1101,6 +1115,7 @@ export class CanonAgent {
1101
1115
  const turnOpenedAt = Date.now();
1102
1116
  let turnState = 'thinking';
1103
1117
  let shouldPersistTurnState = false;
1118
+ let durableMessageSequence = 0;
1104
1119
  const agentId = this.agentId;
1105
1120
  const runtimeState = this.createRuntimeStatePublisher();
1106
1121
  const queueDepth = () => this.sessionManager?.getQueueDepth(conversationId) ?? 0;
@@ -1208,7 +1223,7 @@ export class CanonAgent {
1208
1223
  throwIfAborted();
1209
1224
  const sendOptions = withActiveSelfContext(options);
1210
1225
  const turnTrail = turnOutput.getFinalTrail();
1211
- const result = await this.apiClient.sendMessage(conversationId, text, {
1226
+ const result = await sendDurableMessage(text, {
1212
1227
  ...sendOptions,
1213
1228
  metadata: {
1214
1229
  ...(sendOptions.metadata ?? {}),
@@ -1216,7 +1231,7 @@ export class CanonAgent {
1216
1231
  turnSemantics: 'turn_complete',
1217
1232
  ...(turnTrail.length > 0 ? { turnTrail } : {}),
1218
1233
  },
1219
- });
1234
+ }, ['sdk', 'final', conversationId, turnId]);
1220
1235
  await sleep(FINAL_MESSAGE_HANDOFF_MS);
1221
1236
  try {
1222
1237
  await this.typingSignals.clear(conversationId);
@@ -1266,6 +1281,16 @@ export class CanonAgent {
1266
1281
  return base;
1267
1282
  return activeSelfContextId ? { ...base, selfContextId: activeSelfContextId } : base;
1268
1283
  };
1284
+ const sendDurableMessage = (text, options, fallbackMessageIdParts) => {
1285
+ const messageId = options?.messageId
1286
+ ?? buildSdkMessageId([...fallbackMessageIdParts, durableMessageSequence += 1]);
1287
+ return sendMessageWithRetry(this.apiClient, conversationId, text, {
1288
+ ...(options ?? {}),
1289
+ messageId,
1290
+ }, {
1291
+ sleep: (ms) => sleepWithAbort(ms, abortController.signal),
1292
+ });
1293
+ };
1269
1294
  // Build agent context (fallback to minimal if not yet received)
1270
1295
  const agent = this.agentContext ?? {
1271
1296
  agentId: this.agentId,
@@ -1549,14 +1574,14 @@ export class CanonAgent {
1549
1574
  kind: request.kind,
1550
1575
  reason: result.status,
1551
1576
  });
1552
- await this.apiClient.sendMessage(conversationId, outcome.text, {
1577
+ await sendDurableMessage(outcome.text, {
1553
1578
  metadata: {
1554
1579
  ...outcome.metadata,
1555
1580
  turnId: request.turnId ?? turnId,
1556
1581
  turnSemantics: 'control',
1557
1582
  replyBehavior: 'suppress_auto_reply',
1558
1583
  },
1559
- });
1584
+ }, ['sdk', 'runtime-input-outcome', conversationId, request.turnId ?? turnId, inputId, result.status]);
1560
1585
  throwIfAborted();
1561
1586
  shouldPersistTurnState = false;
1562
1587
  try {
@@ -1725,14 +1750,14 @@ export class CanonAgent {
1725
1750
  const outcome = buildRuntimeCardOutcome(cardId, resolutionStatus, {
1726
1751
  reason: resolutionStatus,
1727
1752
  });
1728
- await this.apiClient.sendMessage(conversationId, outcome.text, {
1753
+ await sendDurableMessage(outcome.text, {
1729
1754
  metadata: {
1730
1755
  ...outcome.metadata,
1731
1756
  turnId: request.turnId ?? turnId,
1732
1757
  turnSemantics: 'control',
1733
1758
  replyBehavior: 'suppress_auto_reply',
1734
1759
  },
1735
- });
1760
+ }, ['sdk', 'runtime-card-outcome', conversationId, request.turnId ?? turnId, cardId, resolutionStatus]);
1736
1761
  throwIfAborted();
1737
1762
  shouldPersistTurnState = false;
1738
1763
  try {
@@ -1754,14 +1779,14 @@ export class CanonAgent {
1754
1779
  if (abortController.signal.aborted || isAbortLikeError(error)) {
1755
1780
  if (shouldSendInterruptedOutcome) {
1756
1781
  const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
1757
- await this.apiClient.sendMessage(conversationId, outcome.text, {
1782
+ await sendDurableMessage(outcome.text, {
1758
1783
  metadata: {
1759
1784
  ...outcome.metadata,
1760
1785
  turnId: request.turnId ?? turnId,
1761
1786
  turnSemantics: 'control',
1762
1787
  replyBehavior: 'suppress_auto_reply',
1763
1788
  },
1764
- }).catch(() => { });
1789
+ }, ['sdk', 'runtime-card-outcome', conversationId, request.turnId ?? turnId, cardId, 'interrupted']).catch(() => { });
1765
1790
  }
1766
1791
  throw error;
1767
1792
  }
@@ -1933,13 +1958,13 @@ export class CanonAgent {
1933
1958
  }
1934
1959
  catch { }
1935
1960
  if (text) {
1936
- await this.apiClient.sendMessage(conversationId, text, {
1961
+ await sendDurableMessage(text, {
1937
1962
  metadata: {
1938
1963
  turnId,
1939
1964
  turnSemantics: 'control',
1940
1965
  replyBehavior: 'suppress_auto_reply',
1941
1966
  },
1942
- });
1967
+ }, ['sdk', 'waiting-input-note', conversationId, turnId]);
1943
1968
  }
1944
1969
  },
1945
1970
  },
package/dist/realtime.js CHANGED
@@ -211,7 +211,13 @@ export class RealtimeManager {
211
211
  if (message.senderId === this.agentId)
212
212
  continue;
213
213
  const createdAtMs = messageCreatedAtMs(message.createdAt);
214
- if (!createdAtMs || createdAtMs <= lowerBoundMs)
214
+ // Use a strict lower bound of `< lowerBoundMs` (not `<=`): a
215
+ // gap-dropped message can share the same createdAt millisecond as
216
+ // the last message seen over SSE (server timestamps collide under
217
+ // bursts — exactly the scenario catch-up targets). The id-dedupe on
218
+ // the next line suppresses the already-delivered boundary message,
219
+ // so excluding by `<=` would only drop never-seen same-ms peers.
220
+ if (!createdAtMs || createdAtMs < lowerBoundMs)
215
221
  continue;
216
222
  if (this.hasSeenInboundMessage(conversation.id, message.id))
217
223
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "3.2.4",
3
+ "version": "3.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",
@@ -28,7 +28,7 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@canonmsg/core": "^2.6.0"
31
+ "@canonmsg/core": "^2.7.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"