@canonmsg/agent-sdk 2.0.0 → 2.1.1

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.
@@ -55,6 +55,7 @@ export declare class CanonAgent {
55
55
  private readonly activeTurns;
56
56
  private readonly conversationMemberIds;
57
57
  private readonly pendingMembershipChanges;
58
+ private readonly typingSignals;
58
59
  private sseConnectedLogged;
59
60
  constructor(options: CanonAgentOptions);
60
61
  private ensureApprovalManager;
@@ -1,7 +1,8 @@
1
- import { ApprovalManager, CanonClient, buildCanonGroupContext, createTurnOutputController, createRuntimeStatePublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeInputOutcome, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, } from '@canonmsg/core';
1
+ import { ApprovalManager, CanonClient, buildCanonGroupContext, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, } from '@canonmsg/core';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { AuthManager } from './auth.js';
4
4
  import { Debouncer } from './debouncer.js';
5
+ import { buildRuntimeCardCreateArgs } from './runtime-card.js';
5
6
  import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, uploadMediaFile, } from './media.js';
6
7
  import { SessionManager } from './session-manager.js';
7
8
  const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
@@ -23,6 +24,17 @@ const DEFAULT_SDK_RUNTIME_DESCRIPTOR = {
23
24
  runtimeControls: [],
24
25
  supportsInterrupt: false,
25
26
  streamingTextMode: 'snapshot',
27
+ runtimeCards: {
28
+ rich: {
29
+ schema: 'canon.card.v1',
30
+ lifecycle: 'blocking_requires_action',
31
+ responder: 'agent_owner',
32
+ result: 'action_or_values',
33
+ maxTimeoutMs: 30 * 60_000,
34
+ blockKinds: ['summary', 'metricGrid', 'chart', 'table', 'list', 'callout', 'actions'],
35
+ native: true,
36
+ },
37
+ },
26
38
  };
27
39
  const STANDARD_PRIMITIVE_COMMANDS = {
28
40
  'runtime.status': {
@@ -146,6 +158,13 @@ function safeRuntimeInputId(value, kind) {
146
158
  ? normalized
147
159
  : `${kind}_${randomUUID()}`;
148
160
  }
161
+ function safeRuntimeCardId(value) {
162
+ const raw = value?.trim() || `card_${randomUUID()}`;
163
+ const normalized = raw.replace(/[.#$\[\]\/\s]+/g, '_').slice(0, 80);
164
+ return RUNTIME_INPUT_ID_PATTERN.test(normalized)
165
+ ? normalized
166
+ : `card_${randomUUID()}`;
167
+ }
149
168
  function isRuntimePrimitiveId(value) {
150
169
  return value === 'runtime.status'
151
170
  || value === 'runtime.reasoning.set'
@@ -240,6 +259,7 @@ export class CanonAgent {
240
259
  activeTurns = new Map();
241
260
  conversationMemberIds = new Map();
242
261
  pendingMembershipChanges = new Map();
262
+ typingSignals;
243
263
  sseConnectedLogged = false;
244
264
  constructor(options) {
245
265
  this.options = {
@@ -252,6 +272,11 @@ export class CanonAgent {
252
272
  ...options,
253
273
  };
254
274
  this.apiClient = new CanonClient(this.options.apiKey, this.options.baseUrl);
275
+ this.typingSignals = createTypingStatusPublisher({
276
+ setTyping: (conversationId, typing, status) => status
277
+ ? this.apiClient.setTyping(conversationId, typing, status)
278
+ : this.apiClient.setTyping(conversationId, typing),
279
+ });
255
280
  this.authManager = new AuthManager(this.apiClient);
256
281
  this.debouncer = new Debouncer(this.options.debounceMs);
257
282
  const apiClient = this.apiClient;
@@ -983,7 +1008,7 @@ export class CanonAgent {
983
1008
  }
984
1009
  await Promise.all([
985
1010
  this.apiClient.clearStreaming(conversationId).catch(() => { }),
986
- this.apiClient.setTyping(conversationId, false).catch(() => { }),
1011
+ this.typingSignals.clear(conversationId).catch(() => { }),
987
1012
  ]);
988
1013
  }
989
1014
  abortActiveTurns(conversationId) {
@@ -1130,14 +1155,13 @@ export class CanonAgent {
1130
1155
  };
1131
1156
  // Show thinking indicator and keep it alive (5s client-side expiry)
1132
1157
  try {
1133
- await this.apiClient.setTyping(conversationId, true, 'thinking');
1158
+ await this.typingSignals.start(conversationId, 'thinking');
1134
1159
  }
1135
1160
  catch {
1136
1161
  // Non-critical
1137
1162
  }
1138
1163
  await setLiveState('thinking', 'Thinking...', 'thinking');
1139
1164
  const thinkingKeepalive = setInterval(() => {
1140
- this.apiClient.setTyping(conversationId, true, 'thinking').catch(() => { });
1141
1165
  if (turnState === 'thinking') {
1142
1166
  turnOutput.setStatus('thinking', 'Thinking...').catch(() => { });
1143
1167
  }
@@ -1170,7 +1194,7 @@ export class CanonAgent {
1170
1194
  const replyFinal = async (text, options) => {
1171
1195
  throwIfAborted();
1172
1196
  try {
1173
- await this.apiClient.setTyping(conversationId, true, 'typing');
1197
+ await this.typingSignals.start(conversationId, 'typing');
1174
1198
  }
1175
1199
  catch { }
1176
1200
  throwIfAborted();
@@ -1187,7 +1211,7 @@ export class CanonAgent {
1187
1211
  });
1188
1212
  await sleep(FINAL_MESSAGE_HANDOFF_MS);
1189
1213
  try {
1190
- await this.apiClient.setTyping(conversationId, false);
1214
+ await this.typingSignals.clear(conversationId);
1191
1215
  }
1192
1216
  catch { }
1193
1217
  return result;
@@ -1336,7 +1360,7 @@ export class CanonAgent {
1336
1360
  catch { }
1337
1361
  await writeTurn('waiting_input');
1338
1362
  try {
1339
- await this.apiClient.setTyping(conversationId, false);
1363
+ await this.typingSignals.clear(conversationId);
1340
1364
  }
1341
1365
  catch { }
1342
1366
  const result = await manager.requestApproval(conversationId, request.toolName, request.toolInput ?? {}, {
@@ -1360,7 +1384,7 @@ export class CanonAgent {
1360
1384
  }
1361
1385
  catch { }
1362
1386
  try {
1363
- await this.apiClient.setTyping(conversationId, true, 'thinking');
1387
+ await this.typingSignals.start(conversationId, 'thinking');
1364
1388
  }
1365
1389
  catch { }
1366
1390
  await setLiveState('thinking', 'Thinking...', 'thinking');
@@ -1427,7 +1451,7 @@ export class CanonAgent {
1427
1451
  catch { }
1428
1452
  await writeTurn('waiting_input');
1429
1453
  try {
1430
- await this.apiClient.setTyping(conversationId, false);
1454
+ await this.typingSignals.clear(conversationId);
1431
1455
  }
1432
1456
  catch { }
1433
1457
  while (Date.now() < expiresAtMs) {
@@ -1501,7 +1525,7 @@ export class CanonAgent {
1501
1525
  }
1502
1526
  catch { }
1503
1527
  try {
1504
- await this.apiClient.setTyping(conversationId, true, 'thinking');
1528
+ await this.typingSignals.start(conversationId, 'thinking');
1505
1529
  }
1506
1530
  catch { }
1507
1531
  await setLiveState('thinking', 'Thinking...', 'thinking');
@@ -1512,6 +1536,193 @@ export class CanonAgent {
1512
1536
  if (abortController.signal.aborted || isAbortLikeError(error)) {
1513
1537
  throw error;
1514
1538
  }
1539
+ if (!requestCreated) {
1540
+ throw error;
1541
+ }
1542
+ shouldPersistTurnState = false;
1543
+ return result;
1544
+ }
1545
+ };
1546
+ const sendCard = async (request) => {
1547
+ throwIfAborted();
1548
+ const cardId = safeRuntimeCardId(request.cardId ?? request.card.cardId);
1549
+ const explicitExpiresAt = request.expiresAt instanceof Date
1550
+ ? request.expiresAt.getTime()
1551
+ : typeof request.expiresAt === 'number'
1552
+ ? request.expiresAt
1553
+ : typeof request.expiresAt === 'string'
1554
+ ? Date.parse(request.expiresAt)
1555
+ : null;
1556
+ const timeoutMs = Math.max(1_000, request.timeoutMs ?? DEFAULT_RUNTIME_INPUT_TIMEOUT_MS);
1557
+ const expiresAtMs = explicitExpiresAt && Number.isFinite(explicitExpiresAt)
1558
+ ? explicitExpiresAt
1559
+ : Date.now() + timeoutMs;
1560
+ // Fire-and-forget: post the durable card and return. The backend treats an
1561
+ // action-less card as display (no pending state, no response expected).
1562
+ await this.apiClient.createRuntimeCardRequest(buildRuntimeCardCreateArgs({ request, conversationId, cardId, fallbackTurnId: turnId, expiresAtMs }));
1563
+ try {
1564
+ await turnOutput.addBlock({
1565
+ id: `card:${cardId}`,
1566
+ kind: 'status',
1567
+ status: 'completed',
1568
+ title: request.card.title,
1569
+ summary: request.card.template ?? 'runtime card',
1570
+ });
1571
+ }
1572
+ catch { }
1573
+ return { status: 'displayed', cardId };
1574
+ };
1575
+ const requestCard = async (request) => {
1576
+ throwIfAborted();
1577
+ // Action-less cards are fire-and-forget display/report cards — never block.
1578
+ const hasActions = Array.isArray(request.card.blocks)
1579
+ && request.card.blocks.some((block) => block.kind === 'actions');
1580
+ if (!hasActions)
1581
+ return sendCard(request);
1582
+ const cardId = safeRuntimeCardId(request.cardId ?? request.card.cardId);
1583
+ const explicitExpiresAt = request.expiresAt instanceof Date
1584
+ ? request.expiresAt.getTime()
1585
+ : typeof request.expiresAt === 'number'
1586
+ ? request.expiresAt
1587
+ : typeof request.expiresAt === 'string'
1588
+ ? Date.parse(request.expiresAt)
1589
+ : null;
1590
+ const timeoutMs = Math.max(1_000, request.timeoutMs ?? DEFAULT_RUNTIME_INPUT_TIMEOUT_MS);
1591
+ const expiresAtMs = explicitExpiresAt && Number.isFinite(explicitExpiresAt)
1592
+ ? explicitExpiresAt
1593
+ : Date.now() + timeoutMs;
1594
+ let result = { status: 'timeout', cardId };
1595
+ let requestCreated = false;
1596
+ let requestResolved = false;
1597
+ const cancelPendingRequest = async () => {
1598
+ if (!requestCreated || requestResolved)
1599
+ return;
1600
+ try {
1601
+ await this.apiClient.consumeRuntimeCardResponse({
1602
+ conversationId,
1603
+ cardId,
1604
+ cancel: true,
1605
+ });
1606
+ requestResolved = true;
1607
+ }
1608
+ catch { }
1609
+ };
1610
+ shouldPersistTurnState = true;
1611
+ try {
1612
+ await this.apiClient.createRuntimeCardRequest(buildRuntimeCardCreateArgs({ request, conversationId, cardId, fallbackTurnId: turnId, expiresAtMs }));
1613
+ requestCreated = true;
1614
+ try {
1615
+ await turnOutput.addBlock({
1616
+ id: `card:${cardId}`,
1617
+ kind: 'input',
1618
+ status: 'pending',
1619
+ title: request.card.title,
1620
+ summary: request.card.template ?? 'runtime card',
1621
+ });
1622
+ await turnOutput.waitingInput();
1623
+ }
1624
+ catch { }
1625
+ await writeTurn('waiting_input');
1626
+ try {
1627
+ await this.typingSignals.clear(conversationId);
1628
+ }
1629
+ catch { }
1630
+ while (Date.now() < expiresAtMs) {
1631
+ throwIfAborted();
1632
+ try {
1633
+ const response = await this.apiClient.consumeRuntimeCardResponse({
1634
+ conversationId,
1635
+ cardId,
1636
+ });
1637
+ if (response.status === 'submitted') {
1638
+ requestResolved = true;
1639
+ result = {
1640
+ status: 'submitted',
1641
+ cardId,
1642
+ ...(response.actionId ? { actionId: response.actionId } : {}),
1643
+ ...(response.values ? { values: response.values } : {}),
1644
+ };
1645
+ break;
1646
+ }
1647
+ if (response.status === 'cancelled' || response.status === 'timeout') {
1648
+ requestResolved = true;
1649
+ result = { status: response.status, cardId };
1650
+ break;
1651
+ }
1652
+ }
1653
+ catch {
1654
+ // Keep waiting; the control path is the authoritative source and
1655
+ // transient consume failures should not leak response values.
1656
+ }
1657
+ await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
1658
+ }
1659
+ if (!requestResolved) {
1660
+ try {
1661
+ const response = await this.apiClient.consumeRuntimeCardResponse({
1662
+ conversationId,
1663
+ cardId,
1664
+ });
1665
+ if (response.status === 'submitted') {
1666
+ result = {
1667
+ status: 'submitted',
1668
+ cardId,
1669
+ ...(response.actionId ? { actionId: response.actionId } : {}),
1670
+ ...(response.values ? { values: response.values } : {}),
1671
+ };
1672
+ }
1673
+ else if (response.status === 'cancelled' || response.status === 'timeout') {
1674
+ result = { status: response.status, cardId };
1675
+ }
1676
+ }
1677
+ catch { }
1678
+ requestResolved = true;
1679
+ }
1680
+ // The interactive path never yields 'displayed' (that returns early via
1681
+ // sendCard); narrow for buildRuntimeCardOutcome's resolution status.
1682
+ const resolutionStatus = result.status === 'displayed' ? 'timeout' : result.status;
1683
+ const outcome = buildRuntimeCardOutcome(cardId, resolutionStatus, {
1684
+ reason: resolutionStatus,
1685
+ });
1686
+ await this.apiClient.sendMessage(conversationId, outcome.text, {
1687
+ metadata: {
1688
+ ...outcome.metadata,
1689
+ turnId: request.turnId ?? turnId,
1690
+ turnSemantics: 'control',
1691
+ replyBehavior: 'suppress_auto_reply',
1692
+ },
1693
+ });
1694
+ throwIfAborted();
1695
+ shouldPersistTurnState = false;
1696
+ try {
1697
+ await turnOutput.completeBlock(`card:${cardId}`, {
1698
+ summary: `Card ${result.status}`,
1699
+ });
1700
+ }
1701
+ catch { }
1702
+ try {
1703
+ await this.typingSignals.start(conversationId, 'thinking');
1704
+ }
1705
+ catch { }
1706
+ await setLiveState('thinking', 'Thinking...', 'thinking');
1707
+ return result;
1708
+ }
1709
+ catch (error) {
1710
+ const shouldSendInterruptedOutcome = requestCreated && !requestResolved;
1711
+ await cancelPendingRequest();
1712
+ if (abortController.signal.aborted || isAbortLikeError(error)) {
1713
+ if (shouldSendInterruptedOutcome) {
1714
+ const outcome = buildRuntimeCardOutcome(cardId, 'cancelled', { reason: 'interrupted' });
1715
+ await this.apiClient.sendMessage(conversationId, outcome.text, {
1716
+ metadata: {
1717
+ ...outcome.metadata,
1718
+ turnId: request.turnId ?? turnId,
1719
+ turnSemantics: 'control',
1720
+ replyBehavior: 'suppress_auto_reply',
1721
+ },
1722
+ }).catch(() => { });
1723
+ }
1724
+ throw error;
1725
+ }
1515
1726
  shouldPersistTurnState = false;
1516
1727
  return result;
1517
1728
  }
@@ -1520,7 +1731,7 @@ export class CanonAgent {
1520
1731
  const replyWithFile = async (filePath, text = '', options) => {
1521
1732
  throwIfAborted();
1522
1733
  try {
1523
- await this.apiClient.setTyping(conversationId, true, 'typing');
1734
+ await this.typingSignals.start(conversationId, 'typing');
1524
1735
  }
1525
1736
  catch { }
1526
1737
  throwIfAborted();
@@ -1550,7 +1761,7 @@ export class CanonAgent {
1550
1761
  }
1551
1762
  finally {
1552
1763
  try {
1553
- await this.apiClient.setTyping(conversationId, false);
1764
+ await this.typingSignals.clear(conversationId);
1554
1765
  }
1555
1766
  catch { }
1556
1767
  }
@@ -1580,6 +1791,8 @@ export class CanonAgent {
1580
1791
  provenance,
1581
1792
  requestApproval,
1582
1793
  requestRuntimeInput,
1794
+ requestCard,
1795
+ sendCard,
1583
1796
  abortSignal: abortController.signal,
1584
1797
  media: {
1585
1798
  materialize: (message = hydratedMessages[hydratedMessages.length - 1], options) => {
@@ -1668,7 +1881,7 @@ export class CanonAgent {
1668
1881
  catch { }
1669
1882
  await writeTurn('waiting_input');
1670
1883
  try {
1671
- await this.apiClient.setTyping(conversationId, false);
1884
+ await this.typingSignals.clear(conversationId);
1672
1885
  }
1673
1886
  catch { }
1674
1887
  if (text) {
@@ -1715,7 +1928,7 @@ export class CanonAgent {
1715
1928
  clearInterval(thinkingKeepalive);
1716
1929
  // Always clear typing when done
1717
1930
  try {
1718
- await this.apiClient.setTyping(conversationId, false);
1931
+ await this.typingSignals.clear(conversationId);
1719
1932
  }
1720
1933
  catch { }
1721
1934
  try {
@@ -0,0 +1,29 @@
1
+ import type { RuntimeCardNativeMetadata, RuntimeCardV1 } from '@canonmsg/core';
2
+ import type { RuntimeCardRequest } from './types';
3
+ /** Arguments passed to `CanonClient.createRuntimeCardRequest`. */
4
+ export interface RuntimeCardCreateArgs {
5
+ conversationId: string;
6
+ card: RuntimeCardV1;
7
+ cardId: string;
8
+ expiresAt: number;
9
+ responseUserId?: string;
10
+ runtimeId?: string;
11
+ turnId?: string;
12
+ native?: RuntimeCardNativeMetadata;
13
+ }
14
+ /**
15
+ * Build the `createRuntimeCardRequest` payload shared by `sendCard` (display)
16
+ * and `requestCard` (interactive).
17
+ *
18
+ * `responseUserId` is OMITTED unless the developer supplied one, so the Canon
19
+ * backend infers a reachable responder (the owner when they are a conversation
20
+ * member, otherwise the sole other member). Pre-filling the owner here would
21
+ * break agent-to-user DMs where the owner is not a member of the conversation.
22
+ */
23
+ export declare function buildRuntimeCardCreateArgs(input: {
24
+ request: RuntimeCardRequest;
25
+ conversationId: string;
26
+ cardId: string;
27
+ fallbackTurnId?: string;
28
+ expiresAtMs: number;
29
+ }): RuntimeCardCreateArgs;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Build the `createRuntimeCardRequest` payload shared by `sendCard` (display)
3
+ * and `requestCard` (interactive).
4
+ *
5
+ * `responseUserId` is OMITTED unless the developer supplied one, so the Canon
6
+ * backend infers a reachable responder (the owner when they are a conversation
7
+ * member, otherwise the sole other member). Pre-filling the owner here would
8
+ * break agent-to-user DMs where the owner is not a member of the conversation.
9
+ */
10
+ export function buildRuntimeCardCreateArgs(input) {
11
+ const { request, conversationId, cardId, fallbackTurnId, expiresAtMs } = input;
12
+ return {
13
+ conversationId,
14
+ cardId,
15
+ card: { ...request.card, cardId },
16
+ expiresAt: expiresAtMs,
17
+ ...(request.responseUserId ? { responseUserId: request.responseUserId } : {}),
18
+ ...(request.runtimeId ? { runtimeId: request.runtimeId } : {}),
19
+ turnId: request.turnId ?? fallbackTurnId,
20
+ ...(request.native ? { native: request.native } : {}),
21
+ };
22
+ }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, SessionRule, ApprovalResult, } from '@canonmsg/core';
2
- import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, SendMessageOptions, SendContextualSelfContextInput, SessionConfig, TurnOutputBlock, TurnOutputBlockInput } from '@canonmsg/core';
1
+ export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, SessionRule, ApprovalResult, } from '@canonmsg/core';
2
+ import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, SendMessageOptions, SendContextualSelfContextInput, SessionConfig, TurnOutputBlock, TurnOutputBlockInput } from '@canonmsg/core';
3
3
  import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
4
4
  export interface ProgressMessageOptions extends SendMessageOptions {
5
5
  /**
@@ -85,6 +85,22 @@ export interface RuntimeInputResult {
85
85
  answers?: RuntimeInputAnswers;
86
86
  inputId: string;
87
87
  }
88
+ export interface RuntimeCardRequest {
89
+ card: RuntimeCardV1;
90
+ cardId?: string;
91
+ expiresAt?: string | number | Date;
92
+ responseUserId?: string;
93
+ timeoutMs?: number;
94
+ runtimeId?: string;
95
+ turnId?: string;
96
+ native?: RuntimeCardNativeMetadata;
97
+ }
98
+ export interface RuntimeCardResult {
99
+ status: 'submitted' | 'cancelled' | 'timeout' | 'displayed';
100
+ cardId: string;
101
+ actionId?: string;
102
+ values?: Record<string, unknown>;
103
+ }
88
104
  export interface MessageHandlerContext {
89
105
  messages: CanonMessage[];
90
106
  history: CanonMessage[];
@@ -144,6 +160,19 @@ export interface MessageHandlerContext {
144
160
  * never persisted in Canon message metadata.
145
161
  */
146
162
  requestRuntimeInput: (request: RuntimeInputRequest) => Promise<RuntimeInputResult>;
163
+ /**
164
+ * Ask the agent owner to review/respond to a generic rich card. The visible
165
+ * card document is redacted for presentation; raw response values return
166
+ * only to the calling runtime through Canon's control path.
167
+ */
168
+ requestCard: (request: RuntimeCardRequest) => Promise<RuntimeCardResult>;
169
+ /**
170
+ * Post a fire-and-forget display/report card and return immediately with
171
+ * `{ status: 'displayed' }` — no waiting-input lifecycle. Use requestCard for
172
+ * cards that expect a response. (requestCard also auto-detects action-less
173
+ * cards and delegates here.)
174
+ */
175
+ sendCard: (request: RuntimeCardRequest) => Promise<RuntimeCardResult>;
147
176
  /** Canon-managed local media access for the current conversation. */
148
177
  media: {
149
178
  materialize: (message?: CanonMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
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": "^1.0.0"
31
+ "@canonmsg/core": "^1.3.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"