@canonmsg/agent-sdk 1.5.2 → 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.
@@ -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 sseConnectedLogged;
58
59
  constructor(options: CanonAgentOptions);
59
60
  private ensureApprovalManager;
60
61
  private filterApprovalReplyMessages;
@@ -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'
@@ -219,6 +240,7 @@ export class CanonAgent {
219
240
  activeTurns = new Map();
220
241
  conversationMemberIds = new Map();
221
242
  pendingMembershipChanges = new Map();
243
+ sseConnectedLogged = false;
222
244
  constructor(options) {
223
245
  this.options = {
224
246
  baseUrl: 'https://api-6m6mlelskq-uc.a.run.app',
@@ -226,6 +248,7 @@ export class CanonAgent {
226
248
  debounceMs: 2000,
227
249
  historyLimit: 50,
228
250
  autoMarkRead: true,
251
+ runtimeControlSurface: 'agent',
229
252
  ...options,
230
253
  };
231
254
  this.apiClient = new CanonClient(this.options.apiKey, this.options.baseUrl);
@@ -533,12 +556,17 @@ export class CanonAgent {
533
556
  this.sessionManager?.dropQueuedMessage(payload.conversationId, payload.messageId);
534
557
  });
535
558
  rtm.setConnectionHandlers({
536
- onConnected: () => this.startRuntimeHeartbeat(),
559
+ onConnected: () => {
560
+ this.startRuntimeHeartbeat();
561
+ if (!this.sseConnectedLogged) {
562
+ this.sseConnectedLogged = true;
563
+ console.log('[canon-sdk] SSE stream connected');
564
+ }
565
+ },
537
566
  onDisconnected: () => this.stopRuntimeHeartbeat(),
538
567
  });
539
568
  this.realtimeManager = rtm;
540
569
  await rtm.start();
541
- console.log('[canon-sdk] SSE stream started');
542
570
  }
543
571
  async createConversation(options) {
544
572
  return this.apiClient.createConversation(options);
@@ -995,7 +1023,7 @@ export class CanonAgent {
995
1023
  return createRuntimeStatePublisher({
996
1024
  agentId: this.agentId,
997
1025
  clientType: this.options.clientType ?? 'generic',
998
- hostMode: false,
1026
+ hostMode: this.options.runtimeControlSurface === 'host',
999
1027
  });
1000
1028
  }
1001
1029
  requireRuntimeStatePublisher() {
@@ -1058,6 +1086,10 @@ export class CanonAgent {
1058
1086
  if (!runtimeState || !agentId)
1059
1087
  return;
1060
1088
  turnState = state;
1089
+ const isOpenTurn = state === 'thinking'
1090
+ || state === 'streaming'
1091
+ || state === 'tool'
1092
+ || state === 'waiting_input';
1061
1093
  await Promise.resolve(runtimeState.writeTurnState(conversationId, {
1062
1094
  turnId,
1063
1095
  state,
@@ -1065,6 +1097,7 @@ export class CanonAgent {
1065
1097
  currentSpeakerId: agentId,
1066
1098
  capabilities: this.buildRuntimeCapabilities(),
1067
1099
  openedAt: turnOpenedAt,
1100
+ ...(isOpenTurn ? { turnUpdatedAt: Date.now() } : {}),
1068
1101
  ...(state === 'completed' || state === 'interrupted' || state === 'idle'
1069
1102
  ? { completedAt: { '.sv': 'timestamp' } }
1070
1103
  : {}),
@@ -1326,6 +1359,138 @@ export class CanonAgent {
1326
1359
  return { decision: 'deny' };
1327
1360
  }
1328
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
+ };
1329
1494
  const uploadFile = (filePath, options) => uploadMediaFile(this.apiClient, conversationId, filePath, options);
1330
1495
  const replyWithFile = async (filePath, text = '', options) => {
1331
1496
  throwIfAborted();
@@ -1388,6 +1553,7 @@ export class CanonAgent {
1388
1553
  selfContexts,
1389
1554
  provenance,
1390
1555
  requestApproval,
1556
+ requestRuntimeInput,
1391
1557
  abortSignal: abortController.signal,
1392
1558
  media: {
1393
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';
@@ -10,6 +10,9 @@ export declare class RealtimeManager {
10
10
  private agentId;
11
11
  private stream;
12
12
  private running;
13
+ private lastSseErrorKey;
14
+ private lastSseErrorAt;
15
+ private suppressedSseErrorCount;
13
16
  private onAgentContext;
14
17
  private onContactRequest;
15
18
  private onContactApproved;
@@ -20,6 +23,7 @@ export declare class RealtimeManager {
20
23
  private onConnected;
21
24
  private onDisconnected;
22
25
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
26
+ private logSseError;
23
27
  setOnAgentContext(cb: (ctx: AgentContext) => void): void;
24
28
  setContactRequestHandlers(handlers: {
25
29
  onContactRequest?: (payload: ContactRequestPayload) => void;
package/dist/realtime.js CHANGED
@@ -9,6 +9,9 @@ export class RealtimeManager {
9
9
  agentId;
10
10
  stream;
11
11
  running = false;
12
+ lastSseErrorKey = null;
13
+ lastSseErrorAt = 0;
14
+ suppressedSseErrorCount = 0;
12
15
  onAgentContext = null;
13
16
  onContactRequest = null;
14
17
  onContactApproved = null;
@@ -85,11 +88,27 @@ export class RealtimeManager {
85
88
  this.onDisconnected?.();
86
89
  },
87
90
  onError: (err) => {
88
- console.error('[canon-sdk] SSE error:', err.message);
91
+ this.logSseError(err);
89
92
  },
90
93
  },
91
94
  });
92
95
  }
96
+ logSseError(err) {
97
+ const code = err.code;
98
+ const key = `${typeof code === 'string' ? code : 'generic'}:${err.message}`;
99
+ const now = Date.now();
100
+ if (this.lastSseErrorKey === key && now - this.lastSseErrorAt < 60_000) {
101
+ this.suppressedSseErrorCount += 1;
102
+ return;
103
+ }
104
+ if (this.suppressedSseErrorCount > 0) {
105
+ console.error(`[canon-sdk] SSE error repeated ${this.suppressedSseErrorCount} more time${this.suppressedSseErrorCount === 1 ? '' : 's'}`);
106
+ this.suppressedSseErrorCount = 0;
107
+ }
108
+ this.lastSseErrorKey = key;
109
+ this.lastSseErrorAt = now;
110
+ console.error('[canon-sdk] SSE error:', err.message);
111
+ }
93
112
  setOnAgentContext(cb) {
94
113
  this.onAgentContext = cb;
95
114
  }
@@ -118,6 +137,5 @@ export class RealtimeManager {
118
137
  stop() {
119
138
  this.running = false;
120
139
  this.stream.stop();
121
- this.onDisconnected?.();
122
140
  }
123
141
  }
@@ -2,7 +2,12 @@ import { evaluateParticipationPolicy, normalizeTurnState, rtdbRead, shouldTrigge
2
2
  function normalizeRuntimeTurnState(value) {
3
3
  const turnState = normalizeTurnState(value);
4
4
  if (turnState) {
5
- return { state: turnState.state };
5
+ return {
6
+ state: turnState.state,
7
+ ...(turnState.openedAt !== undefined ? { openedAt: turnState.openedAt } : {}),
8
+ ...(turnState.updatedAt !== undefined ? { updatedAt: turnState.updatedAt } : {}),
9
+ ...(turnState.turnUpdatedAt !== undefined ? { turnUpdatedAt: turnState.turnUpdatedAt } : {}),
10
+ };
6
11
  }
7
12
  return null;
8
13
  }
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.2",
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.19.3"
31
+ "@canonmsg/core": "^0.20.1"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"