@canonmsg/agent-sdk 8.2.0 → 8.4.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.
package/README.md CHANGED
@@ -476,7 +476,7 @@ While a handler runs, the SDK automatically publishes Canon turn state and clear
476
476
  - `setWaitingInput(text?)`
477
477
  - `noReply(reason?)`
478
478
 
479
- `noReply()` is Canon's `no_reply` verb on the SDK side: end this turn without posting anything. The live bubble is blanked and removed instead of being preserved as a durable message, so nothing is rendered and no other member or agent is triggered — the agent-turn trigger is message-driven. Use it in groups when the handler decides it has nothing to add. `reason` is private: the SDK records that one was given, never the text.
479
+ `noReply()` is Canon's `no_reply` verb on the SDK side: end this turn without posting anything. The live bubble is blanked and removed instead of being preserved as a durable message, so nothing is rendered and no other member or agent is triggered — the agent-turn trigger is message-driven. Use it in groups when the handler decides it has nothing to add. `reason` is private: the text never leaves the process. Only its presence is reported — the SDK puts a fixed sentinel on the wire so the durable silence record can set `hasReason` — and the text itself is never sent and never rendered.
480
480
 
481
481
  It governs teardown only. Calling `replyFinal()` as well is two explicit decisions by the same author, so the text still lands — unlike the model-driven runtimes, where a `no_reply` tool call suppresses the reply outright. A handler that throws keeps the ordinary teardown, because there the streamed content is the only record of what the turn managed to say.
482
482
 
@@ -1,5 +1,5 @@
1
1
  import { type AddMemberResult, type CanonContact, type CanonConversation, type CanonConversationsPage, type CanonConversationsPageOptions, type CreateConversationResult, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ContactCardPayload, type ClearRuntimeActivityOptions, type CreateContactRequestResult, type CanonVoiceSession, type CanonVoiceSessionToken, type CreateVoiceSessionOptions, type VoiceSessionEventPayload } from '@canonmsg/core';
2
- import type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, MessageUpdatedHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
2
+ import type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, MessageUpdatedHandler, ParticipationSuppressedHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
3
3
  /**
4
4
  * Contact-graph operations exposed under `agent.contacts`. Wraps the REST
5
5
  * endpoints in CanonClient — the same surface a human user would hit through
@@ -46,6 +46,7 @@ export declare class CanonAgent {
46
46
  private contactAddedHandler;
47
47
  private contactRemovedHandler;
48
48
  private messageUpdatedHandler;
49
+ private participationSuppressedHandler;
49
50
  private interruptHandler;
50
51
  private stopAndDropHandler;
51
52
  private newSessionHandler;
@@ -106,6 +107,15 @@ export declare class CanonAgent {
106
107
  on(event: 'contactApproved', handler: ContactRequestHandler): void;
107
108
  on(event: 'contactAdded', handler: ContactAddedHandler): void;
108
109
  on(event: 'contactRemoved', handler: ContactRemovedHandler): void;
110
+ /**
111
+ * OBSERVE-ONLY. Fires when the participation gate suppressed a turn this
112
+ * agent would otherwise have been dispatched (cap reached, mention required,
113
+ * agent-to-agent disabled). Never run a turn off this event — the withheld
114
+ * message is deliberately not delivered, and replying to it would defeat the
115
+ * loop protection. It exists so a benched agent can tell deliberate
116
+ * participation policy from a dead stream.
117
+ */
118
+ on(event: 'participationSuppressed', handler: ParticipationSuppressedHandler): void;
109
119
  on(event: 'interrupt', handler: RuntimeSignalHandler): void;
110
120
  on(event: 'stopAndDrop', handler: RuntimeSignalHandler): void;
111
121
  on(event: 'newSession', handler: RuntimeSignalHandler): void;
@@ -162,6 +172,7 @@ export declare class CanonAgent {
162
172
  }>;
163
173
  private handleContactRequestEvent;
164
174
  private handleContactGraphEvent;
175
+ private handleParticipationSuppressedEvent;
165
176
  private handleMessageUpdatedEvent;
166
177
  stop(): Promise<void>;
167
178
  private hasInterruptSupport;
@@ -1,4 +1,4 @@
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, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } 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, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reachOutToCanonContact, reportNoReplyOutcome, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } 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';
@@ -259,6 +259,7 @@ export class CanonAgent {
259
259
  contactAddedHandler = null;
260
260
  contactRemovedHandler = null;
261
261
  messageUpdatedHandler = null;
262
+ participationSuppressedHandler = null;
262
263
  interruptHandler = null;
263
264
  stopAndDropHandler = null;
264
265
  newSessionHandler = null;
@@ -470,6 +471,10 @@ export class CanonAgent {
470
471
  this.contactAddedHandler = handler;
471
472
  return;
472
473
  }
474
+ if (event === 'participationSuppressed') {
475
+ this.participationSuppressedHandler = handler;
476
+ return;
477
+ }
473
478
  if (event === 'interrupt') {
474
479
  this.interruptHandler = handler;
475
480
  if (this.running) {
@@ -719,6 +724,11 @@ export class CanonAgent {
719
724
  rtm.setConversationUpdatedHandler((payload) => {
720
725
  this.handleConversationUpdated(payload);
721
726
  });
727
+ rtm.setParticipationSuppressedHandler((payload) => {
728
+ // Observe-only by construction: this path never touches the debouncer,
729
+ // the message handler, or the turn pipeline.
730
+ void this.handleParticipationSuppressedEvent(payload);
731
+ });
722
732
  rtm.setMessageUpdatedHandler((payload) => {
723
733
  void this.handleMessageUpdatedEvent(payload);
724
734
  });
@@ -818,6 +828,16 @@ export class CanonAgent {
818
828
  console.error('[canon-sdk] Contact-graph handler failed:', error instanceof Error ? error.message : error);
819
829
  }
820
830
  }
831
+ async handleParticipationSuppressedEvent(payload) {
832
+ if (!this.participationSuppressedHandler)
833
+ return;
834
+ try {
835
+ await this.participationSuppressedHandler(payload);
836
+ }
837
+ catch (error) {
838
+ console.error('[canon-sdk] participationSuppressed handler failed:', error instanceof Error ? error.message : error);
839
+ }
840
+ }
821
841
  async handleMessageUpdatedEvent(payload) {
822
842
  if (!this.messageUpdatedHandler)
823
843
  return;
@@ -1252,11 +1272,18 @@ export class CanonAgent {
1252
1272
  // Governs TEARDOWN only — `replyFinal` still sends, because in the SDK the
1253
1273
  // developer's two explicit calls are two explicit decisions.
1254
1274
  let deliberatelySilent = false;
1275
+ // The FIRST noReply call's reason (one silence decision per turn), carried
1276
+ // to teardown where the outcome report fires. Only its presence rides the
1277
+ // wire — see reportNoReplyOutcome's sentinel.
1278
+ let noReplyReason;
1255
1279
  // Set when the handler threw or the turn was aborted. Deliberate silence
1256
1280
  // must never blank the live node on those paths: the streamed content is
1257
1281
  // all that survives a turn that died mid-flight.
1258
1282
  let turnEndedAbnormally = false;
1259
1283
  let durableMessageSequence = 0;
1284
+ // The freshest message in the batch is the turn's trigger — same convention
1285
+ // as the provenance lookup for turn verbosity below.
1286
+ const triggeringMessageId = messages[messages.length - 1]?.id;
1260
1287
  const agentId = this.agentId;
1261
1288
  const runtimeState = this.createRuntimeStatePublisher();
1262
1289
  const queueDepth = () => this.sessionManager?.getQueueDepth(conversationId) ?? 0;
@@ -1309,7 +1336,7 @@ export class CanonAgent {
1309
1336
  //
1310
1337
  // Re-deriving it mid-turn is what must never happen: a turn that started
1311
1338
  // quiet and finished verbose would emit a trail nothing narrated.
1312
- const inboundConversationType = normalizeTurnVerbosityConversationType(provenanceByMessageId?.get(messages[messages.length - 1]?.id ?? '')?.conversation?.type);
1339
+ const inboundConversationType = normalizeTurnVerbosityConversationType(provenanceByMessageId?.get(triggeringMessageId ?? '')?.conversation?.type);
1313
1340
  const turnVerbosity = resolveTurnVerbosity({
1314
1341
  configured: selectConfiguredTurnVerbosity(this.options.turnVerbosity, inboundConversationType),
1315
1342
  conversationType: inboundConversationType,
@@ -1468,6 +1495,10 @@ export class CanonAgent {
1468
1495
  return { turnId, durable: false, messageId: null };
1469
1496
  }
1470
1497
  throwIfAborted();
1498
+ // Durable progress skips `sendDurableMessage` (see the chunking note
1499
+ // below) but it is still a durable message in the timeline, so it must
1500
+ // advance the same counter teardown reads for the silence report.
1501
+ durableMessageSequence += 1;
1471
1502
  const { durable: _durable, ...sendOptions } = options;
1472
1503
  const sendOptionsWithContext = withActiveSelfContext(sendOptions);
1473
1504
  // Progress text is caller-controlled too, so it can also blow past
@@ -1554,8 +1585,13 @@ export class CanonAgent {
1554
1585
  // untouched metadata, so the common case (and the interim→final handoff
1555
1586
  // that keys on that id) is unchanged.
1556
1587
  const sendDurableMessage = async (text, options, fallbackMessageIdParts) => {
1588
+ // Counts every durable send the turn attempts, not just the ones that
1589
+ // needed a generated id: teardown reads `durableMessageSequence` to
1590
+ // decide whether the turn genuinely ended silent, and a reply sent
1591
+ // under an explicit caller messageId must not look like silence.
1592
+ durableMessageSequence += 1;
1557
1593
  const messageId = options?.messageId
1558
- ?? buildSdkMessageId([...fallbackMessageIdParts, durableMessageSequence += 1]);
1594
+ ?? buildSdkMessageId([...fallbackMessageIdParts, durableMessageSequence]);
1559
1595
  const { messageIds } = await sendMessageWithRetryChunked(abortAwareClient, conversationId, text, {
1560
1596
  ...(options ?? {}),
1561
1597
  messageId,
@@ -2022,6 +2058,7 @@ export class CanonAgent {
2022
2058
  ...(options?.mimeType ? { mimeType: options.mimeType } : {}),
2023
2059
  ...(options?.durationMs != null ? { durationMs: options.durationMs } : {}),
2024
2060
  });
2061
+ durableMessageSequence += 1;
2025
2062
  await sleep(FINAL_MESSAGE_HANDOFF_MS);
2026
2063
  return result;
2027
2064
  }
@@ -2146,11 +2183,19 @@ export class CanonAgent {
2146
2183
  },
2147
2184
  noReply: async (reason) => {
2148
2185
  throwIfAborted();
2186
+ // First call wins — one silence decision per turn.
2187
+ if (!deliberatelySilent && reason?.trim()) {
2188
+ noReplyReason = reason;
2189
+ }
2149
2190
  deliberatelySilent = true;
2150
- // `reason` is handler-authored free text: record only that one was
2151
- // given, never the text itself.
2191
+ // `reason` is handler-authored free text: log only that one was given.
2152
2192
  console.error(`[canon-sdk] Turn chose no_reply for ${conversationId}`
2153
2193
  + ` (reason: ${reason?.trim() ? 'given' : 'none'})`);
2194
+ // The outcome report deliberately does NOT fire here: `replyFinal`
2195
+ // still sends after `noReply` (the SDK carve-out — see
2196
+ // TurnController.noReply in types.ts) and the handler may yet throw
2197
+ // or be aborted, so only teardown knows whether the silence was
2198
+ // real. It fires there, once, off the flag set above.
2154
2199
  },
2155
2200
  setTool: async (text) => {
2156
2201
  await writeTurn('tool');
@@ -2224,6 +2269,22 @@ export class CanonAgent {
2224
2269
  // here there is nothing to hand off to, and holding the indicator would
2225
2270
  // read as "started to answer, then gave up".
2226
2271
  const silentTeardown = deliberatelySilent && !turnEndedAbnormally;
2272
+ // Telemetry parity with the server-answered verb path, decided HERE
2273
+ // because only teardown knows how the turn actually ended: `noReply`
2274
+ // does not gate `replyFinal` in the SDK, and the handler may throw or be
2275
+ // aborted after choosing silence — either would make a report sent at
2276
+ // noReply-call time record silence for a turn that delivered or died.
2277
+ // `durableMessageSequence` counts the turn's durable sends: any durable
2278
+ // message means the turn did not end silent, deliberate flag or not.
2279
+ // reportNoReplyOutcome is fire-and-forget and never throws, so a failed
2280
+ // or rejecting report degrades to exactly the pre-report behavior.
2281
+ if (silentTeardown && durableMessageSequence === 0) {
2282
+ reportNoReplyOutcome(this.apiClient, {
2283
+ conversationId,
2284
+ ...(triggeringMessageId ? { messageId: triggeringMessageId } : {}),
2285
+ ...(noReplyReason ? { reason: noReplyReason } : {}),
2286
+ });
2287
+ }
2227
2288
  // Always clear typing when done
2228
2289
  try {
2229
2290
  await this.typingSignals.clear(conversationId);
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  export { CanonAgent } from './canon-agent.js';
2
2
  export type { AgentContactsAPI, AgentConversationsAPI, 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, redactSecrets, } from '@canonmsg/core';
4
- export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeTurnModeActivation, CanonRuntimeTurnModeDescriptor, CanonRuntimeTurnModeScope, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, SessionRule, } from '@canonmsg/core';
4
+ export type { ApprovalConfig, ApprovalNativeRequestMetadata, ApprovalOutcomeMetadata, ApprovalReplyMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalResult, ApprovalRisk, CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeTurnModeActivation, CanonRuntimeTurnModeDescriptor, CanonRuntimeTurnModeScope, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ParticipationSuppressedPayload, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, 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 { CanonAgentTurnVerbosityOption } from './turn-verbosity-option.js';
10
10
  export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonConversationsPage, CanonConversationsPageOptions, CanonReplyContext, CanonSelfContext, CanonTurnContextV2, CanonRuntimeDescriptor, MessageUpdatedPayload, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, CreateConversationResult, DirectSessionSelection, TurnVerbosity, TurnVerbosityConfig, } from '@canonmsg/core';
11
- export type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, FinalMessageResult, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
11
+ export type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, FinalMessageResult, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ParticipationSuppressedHandler, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
@@ -1,4 +1,4 @@
1
- import { type AgentContext, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload, type VoiceSessionEventPayload } from '@canonmsg/core';
1
+ import { type AgentContext, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload, type ParticipationSuppressedPayload, type VoiceSessionEventPayload } from '@canonmsg/core';
2
2
  import { Debouncer } from './debouncer.js';
3
3
  /**
4
4
  * Wraps @canonmsg/core's CanonStream with SDK-specific features:
@@ -20,6 +20,7 @@ export declare class RealtimeManager {
20
20
  private onContactAdded;
21
21
  private onContactRemoved;
22
22
  private onConversationUpdated;
23
+ private onParticipationSuppressed;
23
24
  private onMessageUpdated;
24
25
  private onMessageDeleted;
25
26
  private onConnected;
@@ -43,6 +44,12 @@ export declare class RealtimeManager {
43
44
  onContactRemoved?: (payload: ContactRemovedPayload) => void;
44
45
  }): void;
45
46
  setConversationUpdatedHandler(cb: (payload: ConversationUpdatedPayload) => void): void;
47
+ /**
48
+ * Observe-only notice that Canon's participation gate withheld a turn this
49
+ * agent would otherwise have been dispatched. Never run a turn off it — it
50
+ * exists so an agent can tell deliberate policy from a dead stream.
51
+ */
52
+ setParticipationSuppressedHandler(cb: (payload: ParticipationSuppressedPayload) => void): void;
46
53
  setMessageUpdatedHandler(cb: (payload: MessageUpdatedPayload) => void): void;
47
54
  setMessageDeletedHandler(cb: (payload: {
48
55
  conversationId: string;
package/dist/realtime.js CHANGED
@@ -21,6 +21,7 @@ export class RealtimeManager {
21
21
  onContactAdded = null;
22
22
  onContactRemoved = null;
23
23
  onConversationUpdated = null;
24
+ onParticipationSuppressed = null;
24
25
  onMessageUpdated = null;
25
26
  onMessageDeleted = null;
26
27
  onConnected = null;
@@ -113,6 +114,9 @@ export class RealtimeManager {
113
114
  onConversationUpdated: (payload) => {
114
115
  this.onConversationUpdated?.(payload);
115
116
  },
117
+ onParticipationSuppressed: (payload) => {
118
+ this.onParticipationSuppressed?.(payload);
119
+ },
116
120
  onConnected: () => {
117
121
  this.onConnected?.();
118
122
  },
@@ -180,6 +184,14 @@ export class RealtimeManager {
180
184
  setConversationUpdatedHandler(cb) {
181
185
  this.onConversationUpdated = cb;
182
186
  }
187
+ /**
188
+ * Observe-only notice that Canon's participation gate withheld a turn this
189
+ * agent would otherwise have been dispatched. Never run a turn off it — it
190
+ * exists so an agent can tell deliberate policy from a dead stream.
191
+ */
192
+ setParticipationSuppressedHandler(cb) {
193
+ this.onParticipationSuppressed = cb;
194
+ }
183
195
  setMessageUpdatedHandler(cb) {
184
196
  this.onMessageUpdated = cb;
185
197
  }
package/dist/types.d.ts CHANGED
@@ -68,7 +68,9 @@ export interface TurnController {
68
68
  * message, so nothing is rendered and no other member or agent is triggered —
69
69
  * the agent-turn trigger is message-driven.
70
70
  *
71
- * `reason` is private: recorded as present or absent, never sent or rendered.
71
+ * `reason` is private: the text never leaves the process. Only its presence
72
+ * is reported (a fixed sentinel on the wire), so the durable silence record
73
+ * can set `hasReason` — the text itself is never sent and never rendered.
72
74
  *
73
75
  * Unlike the model-driven runtimes, this does NOT gate `replyFinal`. Calling
74
76
  * both is two explicit decisions by the same author, so the text still lands;
@@ -337,6 +339,13 @@ export interface CanonAgentOptions extends CanonAgentConnectionOptions {
337
339
  export type ContactRequestHandler = (request: import('@canonmsg/core').CanonContactRequest) => void | Promise<void>;
338
340
  export type ContactAddedHandler = (contact: import('@canonmsg/core').ContactAddedPayload) => void | Promise<void>;
339
341
  export type ContactRemovedHandler = (payload: import('@canonmsg/core').ContactRemovedPayload) => void | Promise<void>;
342
+ /**
343
+ * Observe-only notice that the participation gate suppressed a turn this
344
+ * agent would otherwise have been dispatched (cap reached, mention required,
345
+ * agent-to-agent disabled). Never run a turn off this event — it exists so a
346
+ * benched agent can tell deliberate participation policy from a dead stream.
347
+ */
348
+ export type ParticipationSuppressedHandler = (payload: import('@canonmsg/core').ParticipationSuppressedPayload) => void | Promise<void>;
340
349
  /**
341
350
  * Result of `agent.reachOut(card)` — describes which side-effect ran so the
342
351
  * caller can decide what to tell the LLM. `messaged` means the agent opened
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "8.2.0",
3
+ "version": "8.4.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": "^10.0.0"
31
+ "@canonmsg/core": "^10.2.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"