@canonmsg/agent-sdk 1.5.3 → 1.5.4

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,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);
@@ -1001,7 +1023,7 @@ export class CanonAgent {
1001
1023
  return createRuntimeStatePublisher({
1002
1024
  agentId: this.agentId,
1003
1025
  clientType: this.options.clientType ?? 'generic',
1004
- hostMode: false,
1026
+ hostMode: this.options.runtimeControlSurface === 'host',
1005
1027
  });
1006
1028
  }
1007
1029
  requireRuntimeStatePublisher() {
@@ -1337,6 +1359,138 @@ export class CanonAgent {
1337
1359
  return { decision: 'deny' };
1338
1360
  }
1339
1361
  };
1362
+ const requestRuntimeInput = async (request) => {
1363
+ throwIfAborted();
1364
+ const inputId = safeRuntimeInputId(request.inputId, request.kind);
1365
+ const timeoutMs = Math.max(1_000, request.timeoutMs ?? DEFAULT_RUNTIME_INPUT_TIMEOUT_MS);
1366
+ const expiresAtMs = Date.now() + timeoutMs;
1367
+ const expiresAt = new Date(expiresAtMs).toISOString();
1368
+ let result = { status: 'timeout', inputId };
1369
+ let requestCreated = false;
1370
+ let requestResolved = false;
1371
+ const cancelPendingRequest = async () => {
1372
+ if (!requestCreated || requestResolved)
1373
+ return;
1374
+ try {
1375
+ await this.apiClient.consumeRuntimeInputResponse({
1376
+ conversationId,
1377
+ inputId,
1378
+ cancel: true,
1379
+ });
1380
+ requestResolved = true;
1381
+ }
1382
+ catch { }
1383
+ };
1384
+ shouldPersistTurnState = true;
1385
+ try {
1386
+ await this.apiClient.createRuntimeInputRequest({
1387
+ conversationId,
1388
+ inputId,
1389
+ kind: request.kind,
1390
+ expiresAt: expiresAtMs,
1391
+ });
1392
+ requestCreated = true;
1393
+ try {
1394
+ await this.apiClient.clearStreaming(conversationId);
1395
+ }
1396
+ catch { }
1397
+ await writeTurn('waiting_input');
1398
+ try {
1399
+ await this.apiClient.setTyping(conversationId, false);
1400
+ }
1401
+ catch { }
1402
+ const card = buildRuntimeInputRequest(inputId, {
1403
+ kind: request.kind,
1404
+ title: request.title,
1405
+ prompt: request.prompt,
1406
+ ...(request.choices ? { choices: request.choices } : {}),
1407
+ ...(request.secretName ? { secretName: request.secretName } : {}),
1408
+ ...(request.native ? { native: request.native } : {}),
1409
+ expiresAt,
1410
+ ...(request.sensitive !== undefined ? { sensitive: request.sensitive } : {}),
1411
+ });
1412
+ await this.apiClient.sendMessage(conversationId, card.text, {
1413
+ metadata: {
1414
+ ...card.metadata,
1415
+ turnId: request.turnId ?? turnId,
1416
+ turnSemantics: 'control',
1417
+ replyBehavior: 'suppress_auto_reply',
1418
+ },
1419
+ });
1420
+ while (Date.now() < expiresAtMs) {
1421
+ throwIfAborted();
1422
+ try {
1423
+ const response = await this.apiClient.consumeRuntimeInputResponse({
1424
+ conversationId,
1425
+ inputId,
1426
+ });
1427
+ if (response.status === 'submitted') {
1428
+ requestResolved = true;
1429
+ result = {
1430
+ status: 'submitted',
1431
+ value: response.value,
1432
+ inputId,
1433
+ };
1434
+ break;
1435
+ }
1436
+ if (response.status === 'cancelled' || response.status === 'timeout') {
1437
+ requestResolved = true;
1438
+ result = { status: response.status, inputId };
1439
+ break;
1440
+ }
1441
+ }
1442
+ catch {
1443
+ // Transient consume failures should not leak sensitive input or
1444
+ // break the runtime; keep waiting until the explicit timeout.
1445
+ }
1446
+ await sleepWithAbort(Math.min(RUNTIME_INPUT_POLL_MS, Math.max(1, expiresAtMs - Date.now())), abortController.signal);
1447
+ }
1448
+ if (!requestResolved) {
1449
+ try {
1450
+ const response = await this.apiClient.consumeRuntimeInputResponse({
1451
+ conversationId,
1452
+ inputId,
1453
+ });
1454
+ if (response.status === 'submitted') {
1455
+ result = { status: 'submitted', value: response.value, inputId };
1456
+ }
1457
+ else if (response.status === 'cancelled' || response.status === 'timeout') {
1458
+ result = { status: response.status, inputId };
1459
+ }
1460
+ }
1461
+ catch { }
1462
+ requestResolved = true;
1463
+ }
1464
+ const outcome = buildRuntimeInputOutcome(inputId, result.status, {
1465
+ kind: request.kind,
1466
+ reason: result.status,
1467
+ });
1468
+ await this.apiClient.sendMessage(conversationId, outcome.text, {
1469
+ metadata: {
1470
+ ...outcome.metadata,
1471
+ turnId: request.turnId ?? turnId,
1472
+ turnSemantics: 'control',
1473
+ replyBehavior: 'suppress_auto_reply',
1474
+ },
1475
+ });
1476
+ throwIfAborted();
1477
+ shouldPersistTurnState = false;
1478
+ try {
1479
+ await this.apiClient.setTyping(conversationId, true, 'thinking');
1480
+ }
1481
+ catch { }
1482
+ await setLiveState('thinking', 'Thinking...', 'thinking');
1483
+ return result;
1484
+ }
1485
+ catch (error) {
1486
+ await cancelPendingRequest();
1487
+ if (abortController.signal.aborted || isAbortLikeError(error)) {
1488
+ throw error;
1489
+ }
1490
+ shouldPersistTurnState = false;
1491
+ return result;
1492
+ }
1493
+ };
1340
1494
  const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
1341
1495
  const replyWithFile = async (filePath, text = '', options) => {
1342
1496
  throwIfAborted();
@@ -1399,6 +1553,7 @@ export class CanonAgent {
1399
1553
  selfContexts,
1400
1554
  provenance,
1401
1555
  requestApproval,
1556
+ requestRuntimeInput,
1402
1557
  abortSignal: abortController.signal,
1403
1558
  media: {
1404
1559
  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.4",
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.20.1"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"