@canonmsg/agent-sdk 5.0.0 → 5.1.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.
@@ -53,6 +53,15 @@ export declare class CanonAgent {
53
53
  private approvalManager;
54
54
  private approvalManagerAgentId;
55
55
  private approvalManagerOwnerId;
56
+ /**
57
+ * Shared poll/timeout/abort engine for interactive runtime input + card
58
+ * requests. The server request is still created inline (host-specific
59
+ * `native`/`responseUserId`), so the descriptors register a no-op `create`
60
+ * but keep the built-in `cancel`: passing the turn's abort `signal` lets the
61
+ * manager fire `consume({ cancel: true })` on interrupt, so the hosts no
62
+ * longer hand-roll a `cancelPendingRequest`.
63
+ */
64
+ private runtimeRequestManager;
56
65
  private cachedConversationIds;
57
66
  private running;
58
67
  private runtimeHeartbeatTimer;
@@ -66,6 +75,16 @@ export declare class CanonAgent {
66
75
  private sseConnectedLogged;
67
76
  constructor(options: CanonAgentOptions);
68
77
  private ensureApprovalManager;
78
+ /**
79
+ * Shared engine for interactive runtime input + card polling. Unlike approval,
80
+ * these families do not need an owner (the request is created inline with the
81
+ * caller's own `responseUserId` policy), so the manager is always available.
82
+ * The registered descriptors keep the built-in poll/consume/timeout logic and
83
+ * the built-in `cancel` (best-effort `consume({ cancel: true })`), replacing
84
+ * only `create` with a no-op because the request is already created inline.
85
+ * On abort the manager runs that cancel via the passed `signal`.
86
+ */
87
+ private ensureRuntimeRequestManager;
69
88
  private filterApprovalReplyMessages;
70
89
  on(event: 'message', handler: MessageHandler): void;
71
90
  on(event: 'messageUpdated', handler: MessageUpdatedHandler): void;
@@ -1,4 +1,4 @@
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, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, renderCanonHostInboundContent, sendMessageWithRetry, } from '@canonmsg/core';
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, sendMessageWithRetry, } 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';
@@ -10,7 +10,6 @@ const RUNTIME_CONTROL_POLL_INTERVAL_MS = 2_000;
10
10
  const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
11
11
  const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
12
12
  const DEFAULT_RUNTIME_INPUT_TIMEOUT_MS = 5 * 60_000;
13
- const RUNTIME_INPUT_POLL_MS = 1_000;
14
13
  const RUNTIME_INPUT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,160}$/;
15
14
  const SDK_MESSAGE_ID_READABLE_MAX = 120;
16
15
  const SDK_RUNTIME_CAPABILITIES = {
@@ -268,6 +267,15 @@ export class CanonAgent {
268
267
  approvalManager = null;
269
268
  approvalManagerAgentId = null;
270
269
  approvalManagerOwnerId = null;
270
+ /**
271
+ * Shared poll/timeout/abort engine for interactive runtime input + card
272
+ * requests. The server request is still created inline (host-specific
273
+ * `native`/`responseUserId`), so the descriptors register a no-op `create`
274
+ * but keep the built-in `cancel`: passing the turn's abort `signal` lets the
275
+ * manager fire `consume({ cancel: true })` on interrupt, so the hosts no
276
+ * longer hand-roll a `cancelPendingRequest`.
277
+ */
278
+ runtimeRequestManager = null;
271
279
  cachedConversationIds = [];
272
280
  running = false;
273
281
  runtimeHeartbeatTimer = null;
@@ -354,6 +362,33 @@ export class CanonAgent {
354
362
  this.approvalManagerOwnerId = ownerId;
355
363
  return this.approvalManager;
356
364
  }
365
+ /**
366
+ * Shared engine for interactive runtime input + card polling. Unlike approval,
367
+ * these families do not need an owner (the request is created inline with the
368
+ * caller's own `responseUserId` policy), so the manager is always available.
369
+ * The registered descriptors keep the built-in poll/consume/timeout logic and
370
+ * the built-in `cancel` (best-effort `consume({ cancel: true })`), replacing
371
+ * only `create` with a no-op because the request is already created inline.
372
+ * On abort the manager runs that cancel via the passed `signal`.
373
+ */
374
+ ensureRuntimeRequestManager() {
375
+ if (!this.runtimeRequestManager) {
376
+ const manager = new RuntimeRequestManager(this.apiClient, {
377
+ agentId: this.agentId ?? '',
378
+ ownerId: '',
379
+ });
380
+ manager.register('input', {
381
+ ...runtimeInputDescriptor,
382
+ create: async ({ requestId }) => ({ requestId }),
383
+ });
384
+ manager.register('card', {
385
+ ...runtimeCardDescriptor,
386
+ create: async ({ requestId }) => ({ requestId }),
387
+ });
388
+ this.runtimeRequestManager = manager;
389
+ }
390
+ return this.runtimeRequestManager;
391
+ }
357
392
  filterApprovalReplyMessages(conversationId, messages) {
358
393
  const manager = this.ensureApprovalManager();
359
394
  if (!manager) {
@@ -1481,20 +1516,6 @@ export class CanonAgent {
1481
1516
  const expiresAt = new Date(expiresAtMs).toISOString();
1482
1517
  let result = { status: 'timeout', inputId };
1483
1518
  let requestCreated = false;
1484
- let requestResolved = false;
1485
- const cancelPendingRequest = async () => {
1486
- if (!requestCreated || requestResolved)
1487
- return;
1488
- try {
1489
- await this.apiClient.consumeRuntimeInputResponse({
1490
- conversationId,
1491
- inputId,
1492
- cancel: true,
1493
- });
1494
- requestResolved = true;
1495
- }
1496
- catch { }
1497
- };
1498
1519
  shouldPersistTurnState = true;
1499
1520
  try {
1500
1521
  await this.apiClient.createRuntimeInputRequest({
@@ -1529,56 +1550,15 @@ export class CanonAgent {
1529
1550
  await this.typingSignals.clear(conversationId);
1530
1551
  }
1531
1552
  catch { }
1532
- while (Date.now() < expiresAtMs) {
1533
- throwIfAborted();
1534
- try {
1535
- const response = await this.apiClient.consumeRuntimeInputResponse({
1536
- conversationId,
1537
- inputId,
1538
- });
1539
- if (response.status === 'submitted') {
1540
- requestResolved = true;
1541
- result = {
1542
- status: 'submitted',
1543
- value: response.value,
1544
- answers: response.answers,
1545
- inputId,
1546
- };
1547
- break;
1548
- }
1549
- if (response.status === 'cancelled' || response.status === 'timeout') {
1550
- requestResolved = true;
1551
- result = { status: response.status, inputId };
1552
- break;
1553
- }
1554
- }
1555
- catch {
1556
- // Transient consume failures should not leak sensitive input or
1557
- // break the runtime; keep waiting until the explicit timeout.
1558
- }
1559
- await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
1560
- }
1561
- if (!requestResolved) {
1562
- try {
1563
- const response = await this.apiClient.consumeRuntimeInputResponse({
1564
- conversationId,
1565
- inputId,
1566
- });
1567
- if (response.status === 'submitted') {
1568
- result = {
1569
- status: 'submitted',
1570
- value: response.value,
1571
- answers: response.answers,
1572
- inputId,
1573
- };
1574
- }
1575
- else if (response.status === 'cancelled' || response.status === 'timeout') {
1576
- result = { status: response.status, inputId };
1577
- }
1578
- }
1579
- catch { }
1580
- requestResolved = true;
1581
- }
1553
+ // The shared engine owns the poll-with-abort loop + post-deadline
1554
+ // consume; the request was already created above, so the descriptor's
1555
+ // create is a no-op. On abort the manager fires the descriptor's
1556
+ // built-in cancel (consume({ cancel: true })) via the passed signal.
1557
+ result = await this.ensureRuntimeRequestManager().request('input', conversationId, { kind: request.kind }, {
1558
+ requestId: inputId,
1559
+ expiresAt: expiresAtMs,
1560
+ signal: abortController.signal,
1561
+ });
1582
1562
  const outcome = buildRuntimeInputOutcome(inputId, result.status, {
1583
1563
  kind: request.kind,
1584
1564
  reason: result.status,
@@ -1607,8 +1587,16 @@ export class CanonAgent {
1607
1587
  return result;
1608
1588
  }
1609
1589
  catch (error) {
1610
- await cancelPendingRequest();
1611
1590
  if (abortController.signal.aborted || isAbortLikeError(error)) {
1591
+ if (requestCreated) {
1592
+ // Abort landing before the manager wired its cancel (e.g. during
1593
+ // the pre-request writeTurn/typing round-trips) leaves the
1594
+ // server-side request pending + responder-actionable; cancel it so
1595
+ // a late (possibly sensitive) answer can't resolve a dead request.
1596
+ await this.apiClient
1597
+ .consumeRuntimeInputResponse({ conversationId, inputId, cancel: true })
1598
+ .catch(() => { });
1599
+ }
1612
1600
  throw error;
1613
1601
  }
1614
1602
  if (!requestCreated) {
@@ -1670,19 +1658,6 @@ export class CanonAgent {
1670
1658
  let result = { status: 'timeout', cardId };
1671
1659
  let requestCreated = false;
1672
1660
  let requestResolved = false;
1673
- const cancelPendingRequest = async () => {
1674
- if (!requestCreated || requestResolved)
1675
- return;
1676
- try {
1677
- await this.apiClient.consumeRuntimeCardResponse({
1678
- conversationId,
1679
- cardId,
1680
- cancel: true,
1681
- });
1682
- requestResolved = true;
1683
- }
1684
- catch { }
1685
- };
1686
1661
  shouldPersistTurnState = true;
1687
1662
  try {
1688
1663
  await this.apiClient.createRuntimeCardRequest(buildRuntimeCardCreateArgs({ request, conversationId, cardId, fallbackTurnId: turnId, expiresAtMs }));
@@ -1703,56 +1678,16 @@ export class CanonAgent {
1703
1678
  await this.typingSignals.clear(conversationId);
1704
1679
  }
1705
1680
  catch { }
1706
- while (Date.now() < expiresAtMs) {
1707
- throwIfAborted();
1708
- try {
1709
- const response = await this.apiClient.consumeRuntimeCardResponse({
1710
- conversationId,
1711
- cardId,
1712
- });
1713
- if (response.status === 'submitted') {
1714
- requestResolved = true;
1715
- result = {
1716
- status: 'submitted',
1717
- cardId,
1718
- ...(response.actionId ? { actionId: response.actionId } : {}),
1719
- ...(response.values ? { values: response.values } : {}),
1720
- };
1721
- break;
1722
- }
1723
- if (response.status === 'cancelled' || response.status === 'timeout') {
1724
- requestResolved = true;
1725
- result = { status: response.status, cardId };
1726
- break;
1727
- }
1728
- }
1729
- catch {
1730
- // Keep waiting; the control path is the authoritative source and
1731
- // transient consume failures should not leak response values.
1732
- }
1733
- await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
1734
- }
1735
- if (!requestResolved) {
1736
- try {
1737
- const response = await this.apiClient.consumeRuntimeCardResponse({
1738
- conversationId,
1739
- cardId,
1740
- });
1741
- if (response.status === 'submitted') {
1742
- result = {
1743
- status: 'submitted',
1744
- cardId,
1745
- ...(response.actionId ? { actionId: response.actionId } : {}),
1746
- ...(response.values ? { values: response.values } : {}),
1747
- };
1748
- }
1749
- else if (response.status === 'cancelled' || response.status === 'timeout') {
1750
- result = { status: response.status, cardId };
1751
- }
1752
- }
1753
- catch { }
1754
- requestResolved = true;
1755
- }
1681
+ // The shared engine owns the poll-with-abort loop + post-deadline
1682
+ // consume; the request was already created above, so the descriptor's
1683
+ // create is a no-op. On abort the manager fires the descriptor's
1684
+ // built-in cancel (consume({ cancel: true })) via the passed signal.
1685
+ result = await this.ensureRuntimeRequestManager().request('card', conversationId, { card: request.card }, {
1686
+ requestId: cardId,
1687
+ expiresAt: expiresAtMs,
1688
+ signal: abortController.signal,
1689
+ });
1690
+ requestResolved = true;
1756
1691
  // The interactive path never yields 'displayed' (that returns early via
1757
1692
  // sendCard); narrow for buildRuntimeCardOutcome's resolution status.
1758
1693
  const resolutionStatus = result.status === 'displayed' ? 'timeout' : result.status;
@@ -1784,9 +1719,14 @@ export class CanonAgent {
1784
1719
  }
1785
1720
  catch (error) {
1786
1721
  const shouldSendInterruptedOutcome = requestCreated && !requestResolved;
1787
- await cancelPendingRequest();
1788
1722
  if (abortController.signal.aborted || isAbortLikeError(error)) {
1789
1723
  if (shouldSendInterruptedOutcome) {
1724
+ // Cancel the server-side card if the abort landed before the
1725
+ // manager wired its cancel, so a late action can't resolve a dead
1726
+ // request (mirrors the runtime-input path).
1727
+ await this.apiClient
1728
+ .consumeRuntimeCardResponse({ conversationId, cardId, cancel: true })
1729
+ .catch(() => { });
1790
1730
  const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
1791
1731
  await sendDurableMessage(outcome.text, {
1792
1732
  metadata: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "5.0.0",
3
+ "version": "5.1.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": "^4.0.0"
31
+ "@canonmsg/core": "^4.2.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"