@canonmsg/agent-sdk 1.5.3 → 1.5.5

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.
@@ -140,6 +140,7 @@ export declare class CanonAgent {
140
140
  private publishAcceptedRuntimeSignal;
141
141
  private abortActiveTurns;
142
142
  private resolveBatchDeliveryIntent;
143
+ private markQueuedMessagesAccepted;
143
144
  private notifyMessageInterrupt;
144
145
  private createRuntimeStatePublisher;
145
146
  private requireRuntimeStatePublisher;
@@ -1,4 +1,4 @@
1
- import { ApprovalManager, CanonClient, buildCanonGroupContext, createRuntimeStatePublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, initRTDBAuth, rtdbRead, rtdbWrite, normalizeTurnMetadata, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, selectActiveSelfContexts, } from '@canonmsg/core';
1
+ import { ApprovalManager, CanonClient, buildCanonGroupContext, createRuntimeStatePublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeInputOutcome, buildRuntimeInputRequest, 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';
@@ -7,6 +7,9 @@ import { SessionManager } from './session-manager.js';
7
7
  const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
8
8
  const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
9
9
  const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
10
+ const DEFAULT_RUNTIME_INPUT_TIMEOUT_MS = 5 * 60_000;
11
+ const RUNTIME_INPUT_POLL_MS = 1_000;
12
+ const RUNTIME_INPUT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,160}$/;
10
13
  const SDK_RUNTIME_CAPABILITIES = {
11
14
  supportsInterrupt: false,
12
15
  supportsInputInterrupt: false,
@@ -125,6 +128,24 @@ const STANDARD_PRIMITIVE_COMMANDS = {
125
128
  function sleep(ms) {
126
129
  return new Promise((resolve) => setTimeout(resolve, ms));
127
130
  }
131
+ function sleepWithAbort(ms, signal) {
132
+ if (signal.aborted)
133
+ return Promise.reject(createTurnAbortError());
134
+ return new Promise((resolve, reject) => {
135
+ const timer = setTimeout(resolve, ms);
136
+ signal.addEventListener('abort', () => {
137
+ clearTimeout(timer);
138
+ reject(createTurnAbortError());
139
+ }, { once: true });
140
+ });
141
+ }
142
+ function safeRuntimeInputId(value, kind) {
143
+ const raw = value?.trim() || `${kind}_${randomUUID()}`;
144
+ const normalized = raw.replace(/[.#$\[\]\/\s]+/g, '_').slice(0, 160);
145
+ return RUNTIME_INPUT_ID_PATTERN.test(normalized)
146
+ ? normalized
147
+ : `${kind}_${randomUUID()}`;
148
+ }
128
149
  function isRuntimePrimitiveId(value) {
129
150
  return value === 'runtime.status'
130
151
  || value === 'runtime.reasoning.set'
@@ -227,6 +248,7 @@ export class CanonAgent {
227
248
  debounceMs: 2000,
228
249
  historyLimit: 50,
229
250
  autoMarkRead: true,
251
+ runtimeControlSurface: 'agent',
230
252
  ...options,
231
253
  };
232
254
  this.apiClient = new CanonClient(this.options.apiKey, this.options.baseUrl);
@@ -983,6 +1005,14 @@ export class CanonAgent {
983
1005
  ? 'interrupt'
984
1006
  : 'queue';
985
1007
  }
1008
+ async markQueuedMessagesAccepted(conversationId, messages) {
1009
+ await Promise.all(messages.map((message) => {
1010
+ if (!message.id || normalizeTurnMetadata(message.metadata)?.inboundDisposition !== 'queued') {
1011
+ return Promise.resolve();
1012
+ }
1013
+ return this.apiClient.updateMessageDisposition(conversationId, message.id, 'accepted_now').catch(() => { });
1014
+ }));
1015
+ }
986
1016
  async notifyMessageInterrupt(conversationId, abortSignal) {
987
1017
  if (!abortSignal || !this.interruptHandler)
988
1018
  return;
@@ -1001,7 +1031,7 @@ export class CanonAgent {
1001
1031
  return createRuntimeStatePublisher({
1002
1032
  agentId: this.agentId,
1003
1033
  clientType: this.options.clientType ?? 'generic',
1004
- hostMode: false,
1034
+ hostMode: this.options.runtimeControlSurface === 'host',
1005
1035
  });
1006
1036
  }
1007
1037
  requireRuntimeStatePublisher() {
@@ -1037,6 +1067,7 @@ export class CanonAgent {
1037
1067
  async executeHandler(conversationId, messages, session, provenanceByMessageId) {
1038
1068
  if (!this.handler)
1039
1069
  return;
1070
+ await this.markQueuedMessagesAccepted(conversationId, messages);
1040
1071
  const turnId = randomUUID();
1041
1072
  const turnOpenedAt = Date.now();
1042
1073
  let turnState = 'thinking';
@@ -1337,6 +1368,139 @@ export class CanonAgent {
1337
1368
  return { decision: 'deny' };
1338
1369
  }
1339
1370
  };
1371
+ const requestRuntimeInput = async (request) => {
1372
+ throwIfAborted();
1373
+ const inputId = safeRuntimeInputId(request.inputId, request.kind);
1374
+ const timeoutMs = Math.max(1_000, request.timeoutMs ?? DEFAULT_RUNTIME_INPUT_TIMEOUT_MS);
1375
+ const expiresAtMs = Date.now() + timeoutMs;
1376
+ const expiresAt = new Date(expiresAtMs).toISOString();
1377
+ let result = { status: 'timeout', inputId };
1378
+ let requestCreated = false;
1379
+ let requestResolved = false;
1380
+ const cancelPendingRequest = async () => {
1381
+ if (!requestCreated || requestResolved)
1382
+ return;
1383
+ try {
1384
+ await this.apiClient.consumeRuntimeInputResponse({
1385
+ conversationId,
1386
+ inputId,
1387
+ cancel: true,
1388
+ });
1389
+ requestResolved = true;
1390
+ }
1391
+ catch { }
1392
+ };
1393
+ shouldPersistTurnState = true;
1394
+ try {
1395
+ await this.apiClient.createRuntimeInputRequest({
1396
+ conversationId,
1397
+ inputId,
1398
+ kind: request.kind,
1399
+ expiresAt: expiresAtMs,
1400
+ });
1401
+ requestCreated = true;
1402
+ try {
1403
+ await this.apiClient.clearStreaming(conversationId);
1404
+ }
1405
+ catch { }
1406
+ await writeTurn('waiting_input');
1407
+ try {
1408
+ await this.apiClient.setTyping(conversationId, false);
1409
+ }
1410
+ catch { }
1411
+ const card = buildRuntimeInputRequest(inputId, {
1412
+ kind: request.kind,
1413
+ responseUserId: agent.ownerId,
1414
+ title: request.title,
1415
+ prompt: request.prompt,
1416
+ ...(request.choices ? { choices: request.choices } : {}),
1417
+ ...(request.secretName ? { secretName: request.secretName } : {}),
1418
+ ...(request.native ? { native: request.native } : {}),
1419
+ expiresAt,
1420
+ ...(request.sensitive !== undefined ? { sensitive: request.sensitive } : {}),
1421
+ });
1422
+ await this.apiClient.sendMessage(conversationId, card.text, {
1423
+ metadata: {
1424
+ ...card.metadata,
1425
+ turnId: request.turnId ?? turnId,
1426
+ turnSemantics: 'control',
1427
+ replyBehavior: 'suppress_auto_reply',
1428
+ },
1429
+ });
1430
+ while (Date.now() < expiresAtMs) {
1431
+ throwIfAborted();
1432
+ try {
1433
+ const response = await this.apiClient.consumeRuntimeInputResponse({
1434
+ conversationId,
1435
+ inputId,
1436
+ });
1437
+ if (response.status === 'submitted') {
1438
+ requestResolved = true;
1439
+ result = {
1440
+ status: 'submitted',
1441
+ value: response.value,
1442
+ inputId,
1443
+ };
1444
+ break;
1445
+ }
1446
+ if (response.status === 'cancelled' || response.status === 'timeout') {
1447
+ requestResolved = true;
1448
+ result = { status: response.status, inputId };
1449
+ break;
1450
+ }
1451
+ }
1452
+ catch {
1453
+ // Transient consume failures should not leak sensitive input or
1454
+ // break the runtime; keep waiting until the explicit timeout.
1455
+ }
1456
+ await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
1457
+ }
1458
+ if (!requestResolved) {
1459
+ try {
1460
+ const response = await this.apiClient.consumeRuntimeInputResponse({
1461
+ conversationId,
1462
+ inputId,
1463
+ });
1464
+ if (response.status === 'submitted') {
1465
+ result = { status: 'submitted', value: response.value, inputId };
1466
+ }
1467
+ else if (response.status === 'cancelled' || response.status === 'timeout') {
1468
+ result = { status: response.status, inputId };
1469
+ }
1470
+ }
1471
+ catch { }
1472
+ requestResolved = true;
1473
+ }
1474
+ const outcome = buildRuntimeInputOutcome(inputId, result.status, {
1475
+ kind: request.kind,
1476
+ reason: result.status,
1477
+ });
1478
+ await this.apiClient.sendMessage(conversationId, outcome.text, {
1479
+ metadata: {
1480
+ ...outcome.metadata,
1481
+ turnId: request.turnId ?? turnId,
1482
+ turnSemantics: 'control',
1483
+ replyBehavior: 'suppress_auto_reply',
1484
+ },
1485
+ });
1486
+ throwIfAborted();
1487
+ shouldPersistTurnState = false;
1488
+ try {
1489
+ await this.apiClient.setTyping(conversationId, true, 'thinking');
1490
+ }
1491
+ catch { }
1492
+ await setLiveState('thinking', 'Thinking...', 'thinking');
1493
+ return result;
1494
+ }
1495
+ catch (error) {
1496
+ await cancelPendingRequest();
1497
+ if (abortController.signal.aborted || isAbortLikeError(error)) {
1498
+ throw error;
1499
+ }
1500
+ shouldPersistTurnState = false;
1501
+ return result;
1502
+ }
1503
+ };
1340
1504
  const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
1341
1505
  const replyWithFile = async (filePath, text = '', options) => {
1342
1506
  throwIfAborted();
@@ -1399,6 +1563,7 @@ export class CanonAgent {
1399
1563
  selfContexts,
1400
1564
  provenance,
1401
1565
  requestApproval,
1566
+ requestRuntimeInput,
1402
1567
  abortSignal: abortController.signal,
1403
1568
  media: {
1404
1569
  materialize: (message = hydratedMessages[hydratedMessages.length - 1], options) => {
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
2
  export type { AgentContactsAPI, AgentUsersAPI } from './canon-agent.js';
3
3
  export { ApprovalManager, buildApprovalOutcome, buildApprovalReply, buildApprovalRequest, CanonApiError, DEFAULT_APPROVAL_CONFIG, generateApprovalId, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, parseApprovalReplyMetadata, parseApprovalRequestMetadata, parseSessionRule, parseTextApprovalReply, redactSecrets, } from '@canonmsg/core';
4
- export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, SessionRule, } from '@canonmsg/core';
4
+ export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, SessionRule, } from '@canonmsg/core';
5
5
  export { SessionManager } from './session-manager.js';
6
6
  export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
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, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
10
- export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
10
+ export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
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, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, } from '@canonmsg/core';
2
- import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } 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, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, SessionRule, ApprovalResult, } from '@canonmsg/core';
2
+ import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
3
3
  import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
4
4
  export interface ProgressMessageOptions extends SendMessageOptions {
5
5
  /**
@@ -56,6 +56,24 @@ export interface RuntimeApprovalRequest {
56
56
  /** Whether an approval reply may set an in-memory session rule. */
57
57
  allowSessionRule?: boolean;
58
58
  }
59
+ export interface RuntimeInputRequest {
60
+ kind: RuntimeInputKind;
61
+ title: string;
62
+ prompt: string;
63
+ choices?: RuntimeInputChoice[];
64
+ secretName?: string;
65
+ sensitive?: boolean;
66
+ runtimeId?: string;
67
+ turnId?: string;
68
+ native?: RuntimeInputNativeMetadata;
69
+ inputId?: string;
70
+ timeoutMs?: number;
71
+ }
72
+ export interface RuntimeInputResult {
73
+ status: 'submitted' | 'cancelled' | 'timeout';
74
+ value?: string;
75
+ inputId: string;
76
+ }
59
77
  export interface MessageHandlerContext {
60
78
  messages: CanonMessage[];
61
79
  history: CanonMessage[];
@@ -109,6 +127,12 @@ export interface MessageHandlerContext {
109
127
  * and enforce the returned decision in their own approval hook.
110
128
  */
111
129
  requestApproval: (request: RuntimeApprovalRequest) => Promise<ApprovalResult>;
130
+ /**
131
+ * Ask the agent owner for runtime input such as clarification, sudo, or a
132
+ * secret. Sensitive values are returned only to the calling runtime and are
133
+ * never persisted in Canon message metadata.
134
+ */
135
+ requestRuntimeInput: (request: RuntimeInputRequest) => Promise<RuntimeInputResult>;
112
136
  /** Canon-managed local media access for the current conversation. */
113
137
  media: {
114
138
  materialize: (message?: CanonMessage, options?: Omit<MaterializeMediaOptions, 'agentId' | 'conversationId' | 'messageId'>) => Promise<MaterializedCanonAttachment[]>;
@@ -155,6 +179,7 @@ export interface RuntimeControlHandlers {
155
179
  onStopAndDrop?: RuntimeSignalHandler;
156
180
  onNewSession?: RuntimeSignalHandler;
157
181
  }
182
+ export type RuntimeControlSurface = 'agent' | 'host';
158
183
  export interface RuntimePrimitiveContext {
159
184
  conversationId: string;
160
185
  primitive: CanonRuntimePrimitiveId;
@@ -186,6 +211,8 @@ export interface CanonAgentOptions {
186
211
  runtimeDescriptor?: import('@canonmsg/core').CanonRuntimeDescriptor;
187
212
  /** Optional Canon runtime signal handlers. Enables interrupt controls when provided. */
188
213
  runtimeControls?: RuntimeControlHandlers;
214
+ /** Runtime publishing surface. Use `host` when this agent owns live runtime controls. */
215
+ runtimeControlSurface?: RuntimeControlSurface;
189
216
  /** Optional typed runtime primitive handlers. Enables descriptor-backed command controls when provided. */
190
217
  runtimePrimitives?: RuntimePrimitiveHandlers;
191
218
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
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": "^0.20.0"
31
+ "@canonmsg/core": "^0.21.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"