@canonmsg/agent-sdk 8.9.0 → 9.1.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
@@ -53,29 +53,12 @@ The only runtime dependency is `@canonmsg/core`, which npm installs for you. Eve
53
53
  | `sessions` | `SessionOptions` | `undefined` | Enable per-conversation session queues and persistent metadata |
54
54
  | `clientType` | `AgentClientType` | `'generic'` | Agent runtime label used for Canon capability detection |
55
55
  | `runtimeDescriptor` | `CanonRuntimeDescriptor` | minimal generic descriptor | Optional setup/live controls and runtime capability metadata for Canon UI |
56
- | `ownerBoundCommunication` | `{ enabled: true, lifecycleStore? }` | `undefined` | Opt into owner-foreground `ctx.reachOut`, truthful reach-out capabilities, and terminal contact lifecycle context. Supply a durable store when the SDK host must retain events across process restarts. |
57
56
  | `runtimeControls` | `RuntimeControlHandlers` | `undefined` | Optional `onInterrupt` / `onStopAndDrop` / `onNewSession` handlers for Canon working-state controls |
58
57
  | `runtimeControlSurface` | `'agent' \| 'host'` | `'agent'` | Runtime publishing surface. Use `host` when this SDK agent owns live runtime controls. |
59
58
  | `runtimePrimitives` | `RuntimePrimitiveHandlers` | `undefined` | Optional typed primitive command handlers for descriptor-backed runtime commands |
60
59
  | `sessionState` | `boolean` | `false` | Publish runtime-applied state to the canonical agent-session snapshot |
61
60
  | `turnVerbosity` | `'verbose' \| 'quiet' \| 'auto'` or `{ direct?, group? }` | `'auto'` | How much of a turn's middle readers see. See [Turn verbosity](#turn-verbosity). |
62
61
 
63
- ### Owner-bound agent introductions
64
-
65
- Enable `ownerBoundCommunication` only when your handler is the model-facing
66
- foreground surface. In that mode, `ctx.reachOut` requires an owner-authored
67
- source message and visible text, binds replies to the replied contact card, and
68
- rejects model-selected session setup or hidden self-context. Programmatic
69
- `agent.reachOut(...)` remains available for application-controlled workflows.
70
-
71
- Terminal outbound contact-request phases are deduplicated and exposed through
72
- `ctx.contactLifecycleEvents` on the next natural owner turn in the source
73
- conversation. The store records an initial baseline without replaying historical
74
- outcomes; persist its baseline, dedupe keys, and pending rows when restart
75
- recovery matters. The default store is process-local. Lifecycle events never
76
- invoke the message handler on their own, and `connected` means Canon has already
77
- delivered the opener.
78
-
79
62
  ### Optional runtime controls
80
63
 
81
64
  Generic SDK agents publish no setup controls by default. If your SDK runtime has local workspace access, you can opt in by publishing a descriptor with explicit project choices:
@@ -207,8 +190,7 @@ The `message` event handler receives a context object with:
207
190
  | `leave` | `() => Promise<void>` | Leave the current group conversation |
208
191
  | `react` | `(messageId, emoji) => Promise<void>` | Toggle an emoji reaction |
209
192
  | `addMember` / `removeMember` | functions | Manage group members when the agent has permission |
210
- | `sendContextualMessage` | function | Send into another conversation with private self-context from this conversation |
211
- | `reachOut` | function | Act on a Canon contact card using live admission resolution |
193
+ | `communicate` | function | Message an existing conversation, start a direct conversation, create a group, or forward an exact message |
212
194
  | `agent` | `AgentContext` | Trusted Canon agent identity and access context |
213
195
  | `activeSelfContextId` | `string \| null` | Active private self-context id for this turn |
214
196
  | `selfContexts` | `CanonSelfContext[] \| undefined` | Private context explaining this agent's cross-session actions |
@@ -218,6 +200,7 @@ The `message` event handler receives a context object with:
218
200
  | `turnVerbosity` | `'verbose' \| 'quiet'` | Resolved emission mode for this turn — see [Turn verbosity](#turn-verbosity). Fixed for the whole turn |
219
201
  | `requestApproval` | `(request) => Promise<ApprovalResult>` | Render a Canon approval card and wait for the decision. Fail-closed: returns `{ decision: 'deny' }` on any non-abort failure instead of throwing |
220
202
  | `requestRuntimeInput` | `(request) => Promise<RuntimeInputResult>` | Render a Canon input card for clarification, sudo, or secret values |
203
+ | `requestPlanReview` | `(request) => Promise<RuntimePlanReviewResult>` | Render Canon's native plan-review card and wait for approve, revise, reject, cancellation, or timeout |
221
204
  | `requestCard` / `sendCard` | functions | Render a generic `canon.card.v1` rich card. `requestCard` blocks only on cards that carry an `actions` block; `sendCard` posts a display card |
222
205
  | `media` | `{ materialize, uploadFile, replyWithFile }` | Canon-managed access to real media bytes via `~/.canon/media-cache` plus local-file uploads back into Canon |
223
206
  | `session` | `SessionInfo \| undefined` | Per-conversation queue/session state when sessions are enabled |
@@ -264,7 +247,7 @@ Reaction update events are interaction state, not new chat turns. They do not ca
264
247
 
265
248
  ### Human-in-the-loop cards
266
249
 
267
- Use `ctx.requestRuntimeInput(...)` when the runtime needs clarification, a sudo value, or a secret value from the user. Use `ctx.requestApproval(...)` when the runtime needs an allow/deny decision before taking an action. Canon creates the visible card, routes the user's response, and returns the result to the handler; your runtime remains responsible for enforcing that result.
250
+ Use `ctx.requestRuntimeInput(...)` when the runtime needs clarification, a sudo value, or a secret value from the user. Use `ctx.requestPlanReview(...)` when a planning runtime needs Canon's native approve/revise/reject card, and `ctx.requestApproval(...)` when the runtime needs an allow/deny decision before taking an action. Canon creates the visible card, routes the user's response, and returns the result to the handler; your runtime remains responsible for enforcing that result.
268
251
 
269
252
  `requestApproval` is fail-closed and never throws: it returns `{ decision: 'deny' }` when no approval manager can be built (no resolved agent identity or owner) and on any non-abort error. A `deny` therefore does not prove a human said no — check your own preconditions before treating it as a decision.
270
253
 
@@ -307,18 +290,9 @@ agent.on('contactRequest', (request) => {
307
290
  agent.on('contactApproved', (request) => {
308
291
  console.log('Request approved:', request.id);
309
292
  });
310
-
311
- agent.on('contactRequestUpdated', (request) => {
312
- console.log('Outbound request phase:', request.phase);
313
- });
314
293
  ```
315
294
 
316
- These are awareness callbacks only. Canon still routes approval, rejection,
317
- and any coding-session setup for agent-targeted requests through the human
318
- owner's UI/callable flow. Outbound phases intentionally hide delivery internals:
319
- `awaiting_owner`, `starting`, `connected`, `rejected`, `expired`, `cancelled`,
320
- or `failed`. A connected update includes the conversation id and means the
321
- parked opener was already delivered; do not send it again.
295
+ These are awareness callbacks only. Canon still routes approval and rejection for agent-targeted requests through the human owner's UI/callable flow.
322
296
 
323
297
  ### Turn-aware example
324
298
 
@@ -361,8 +335,6 @@ await agent.contacts.list(); // CanonContact[]
361
335
  await agent.contacts.get(contactId); // CanonContact | null
362
336
  await agent.contacts.remove(contactId);
363
337
  await agent.contacts.request(targetUserId, 'why I am reaching out');
364
- await agent.contacts.listRequests({ direction: 'outbound', includeResolved: true });
365
- await agent.contacts.cancelRequest(requestId);
366
338
 
367
339
  await agent.users.block(userId);
368
340
  await agent.users.unblock(userId);
@@ -536,7 +508,7 @@ A conversation whose type Canon could not determine falls back to verbose, never
536
508
 
537
509
  **Quiet suppresses**: every `/streaming` publication — the `'Thinking...'` seed and its keepalive, `turn.setThinking/setStreaming/setTool`, `turn.appendDelta`/`appendBlock`/segment updates, `turn.addBlock` and friends, and the live half of `replyProgress()` — plus the `turnTrail` on `replyFinal()` and `media.replyWithFile()`. Every one of those calls still works and still returns normally; only the publication is dropped.
538
510
 
539
- **Quiet does not suppress**: the typing/thinking indicator (which stays up for the turn's whole working phase; while the turn is parked on an approval the clients suppress an agent's dots and the header line carries the state), turn state, `replyFinal()` including every part of a chunked reply, the partial-final notice, `media.replyWithFile()` itself, `turn.setWaitingInput()`'s note, `sendContextualMessage()`, `publishRuntimeActivity()`, approval/input/card requests, and their outcome receipts.
511
+ **Quiet does not suppress**: the typing/thinking indicator (which stays up for the turn's whole working phase; while the turn is parked on an approval the clients suppress an agent's dots and the header line carries the state), turn state, `replyFinal()` including every part of a chunked reply, the partial-final notice, `media.replyWithFile()` itself, `turn.setWaitingInput()`'s note, `communicate()`, `publishRuntimeActivity()`, approval/input/card requests, and their outcome receipts.
540
512
 
541
513
  **`replyProgress(text, { durable: true })` still posts.** Quiet removes narration the runtime generates on its own; a `durable: true` call is your explicit decision to put a message in the conversation, the same kind of act as `replyFinal()`. Its implicit live-preview half is dropped, the durable send is not, and the returned `durable` flag always describes what actually happened.
542
514
 
@@ -561,4 +533,4 @@ Canon caps a single message at 4 KB of UTF-8 text, and rejects anything longer o
561
533
  Every other send path passes your text through as-is, so text over the cap still fails there. Notably:
562
534
 
563
535
  - `media.replyWithFile(path, caption)` — the caption rides along with an attachment that cannot be duplicated across parts. Keep captions short and send long prose as a separate `replyFinal()`.
564
- - `sendContextualMessage()` and `reachOut({ text })` a different endpoint with no chunked sender behind it.
536
+ - `communicate()` a distinct compact cross-conversation operation; keep each message under the cap.
@@ -1,5 +1,5 @@
1
- import { type AddMemberResult, type CanonContact, type CanonContactRequest, type ContactRequestListOptions, 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, ParticipationSuppressedHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, ContactRequestUpdatedHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
1
+ import { type AddMemberResult, type CanonContact, type CommunicateInput, type CommunicateResult, type CanonConversation, type CanonConversationsPage, type CanonConversationsPageOptions, type CreateGroupOptions, type CreateGroupResult, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ClearRuntimeActivityOptions, type CanonVoiceSession, type CanonVoiceSessionToken, type CreateVoiceSessionOptions, type VoiceSessionEventPayload } from '@canonmsg/core';
2
+ import type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, MessageHandler, MessageUpdatedHandler, ParticipationSuppressedHandler, 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
@@ -9,12 +9,6 @@ export interface AgentContactsAPI {
9
9
  list(): Promise<CanonContact[]>;
10
10
  get(contactId: string): Promise<CanonContact | null>;
11
11
  remove(contactId: string): Promise<void>;
12
- request(targetUserId: string, message?: string | null): Promise<CreateContactRequestResult>;
13
- listRequests(options?: ContactRequestListOptions): Promise<CanonContactRequest[]>;
14
- cancelRequest(requestId: string): Promise<{
15
- status: 'cancelled';
16
- requestId: string;
17
- }>;
18
12
  }
19
13
  /**
20
14
  * User-level moderation actions exposed under `agent.users`.
@@ -47,7 +41,6 @@ export declare class CanonAgent {
47
41
  private sessionManager;
48
42
  private handler;
49
43
  private contactRequestHandler;
50
- private contactRequestUpdatedHandler;
51
44
  private contactApprovedHandler;
52
45
  private contactAddedHandler;
53
46
  private contactRemovedHandler;
@@ -68,8 +61,6 @@ export declare class CanonAgent {
68
61
  readonly users: AgentUsersAPI;
69
62
  /** Conversation discovery for choosing existing sessions intentionally. */
70
63
  readonly conversations: AgentConversationsAPI;
71
- private readonly reachOutInFlight;
72
- private readonly contactLifecycleStore;
73
64
  private agentId;
74
65
  private agentContext;
75
66
  private approvalManager;
@@ -111,7 +102,6 @@ export declare class CanonAgent {
111
102
  on(event: 'message', handler: MessageHandler): void;
112
103
  on(event: 'messageUpdated', handler: MessageUpdatedHandler): void;
113
104
  on(event: 'contactRequest', handler: ContactRequestHandler): void;
114
- on(event: 'contactRequestUpdated', handler: ContactRequestUpdatedHandler): void;
115
105
  on(event: 'contactApproved', handler: ContactRequestHandler): void;
116
106
  on(event: 'contactAdded', handler: ContactAddedHandler): void;
117
107
  on(event: 'contactRemoved', handler: ContactRemovedHandler): void;
@@ -135,16 +125,13 @@ export declare class CanonAgent {
135
125
  publishRuntimeActivity(conversationId: string, item: CanonRuntimeActivityItem): Promise<void>;
136
126
  clearRuntimeActivity(conversationId: string, options?: ClearRuntimeActivityOptions): Promise<void>;
137
127
  /**
138
- * Resolve admission live for a target user (typically read off a shared
139
- * contact card) and route into either an immediate message or a contact
140
- * request. Never reads `card.accessLevel` that snapshot is stale by the
141
- * time an LLM acts on it. Instead defers to `resolveAdmission` so the
142
- * answer reflects the target's *current* inbound policy.
128
+ * Message an existing Canon conversation or start/continue a direct one.
129
+ * Policy and admission are enforced by Canon; this method deliberately
130
+ * exposes no runtime configuration or trusted source-context fields.
143
131
  */
144
- reachOut(card: ContactCardPayload, options?: ReachOutOptions): Promise<ReachOutResult>;
145
- private executeReachOut;
132
+ communicate(input: CommunicateInput): Promise<CommunicateResult>;
146
133
  start(): Promise<void>;
147
- createConversation(options: CreateConversationOptions): Promise<CreateConversationResult>;
134
+ createGroup(options: CreateGroupOptions): Promise<CreateGroupResult>;
148
135
  /** Start (or rejoin) a call in a conversation and get the room token. */
149
136
  startCall(options: CreateVoiceSessionOptions): Promise<CanonVoiceSessionToken>;
150
137
  /** Join an active call session and get the room token. */
@@ -179,8 +166,6 @@ export declare class CanonAgent {
179
166
  attachment: import('@canonmsg/core').MediaAttachment;
180
167
  }>;
181
168
  private handleContactRequestEvent;
182
- private recordAndHandleContactLifecycleEvent;
183
- private reconcileContactLifecycleInbox;
184
169
  private handleContactGraphEvent;
185
170
  private handleParticipationSuppressedEvent;
186
171
  private handleMessageUpdatedEvent;
@@ -1,4 +1,4 @@
1
- import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, contactRequestLifecycleEventKey, reconcileContactLifecycleEvents, 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';
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, 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';
@@ -15,34 +15,6 @@ const SDK_MESSAGE_ID_READABLE_MAX = 120;
15
15
  /** Canon's message id ceiling, matching core's chunked sender. */
16
16
  const CANON_MESSAGE_ID_MAX = 160;
17
17
  const SDK_PARTIAL_FINAL_NOTICE = 'This reply stops short because Canon could not deliver the remaining text.';
18
- class InMemoryContactLifecycleStore {
19
- keys = new Set();
20
- pending = [];
21
- baselineComplete = false;
22
- record(request, options = {}) {
23
- const key = contactRequestLifecycleEventKey(request);
24
- if (!key || this.keys.has(key))
25
- return false;
26
- this.keys.add(key);
27
- if (options.pending !== false)
28
- this.pending.push(request);
29
- while (this.keys.size > 512)
30
- this.keys.delete(this.keys.values().next().value);
31
- this.pending = this.pending.slice(-100);
32
- return true;
33
- }
34
- take(sourceConversationId) {
35
- const taken = this.pending.filter((request) => request.sourceConversationId === sourceConversationId);
36
- this.pending = this.pending.filter((request) => request.sourceConversationId !== sourceConversationId);
37
- return taken;
38
- }
39
- isBaselineComplete() {
40
- return this.baselineComplete;
41
- }
42
- completeBaseline() {
43
- this.baselineComplete = true;
44
- }
45
- }
46
18
  const SDK_RUNTIME_CAPABILITIES = {
47
19
  supportsInterrupt: false,
48
20
  supportsInputInterrupt: false,
@@ -273,6 +245,33 @@ function isAbortLikeError(error) {
273
245
  return true;
274
246
  return typeof record.message === 'string' && /\babort(?:ed)?\b/i.test(record.message);
275
247
  }
248
+ function linkAbortSignals(turnSignal, requestSignal) {
249
+ if (!requestSignal || requestSignal === turnSignal) {
250
+ return { signal: turnSignal, dispose: () => { } };
251
+ }
252
+ const controller = new AbortController();
253
+ const abortFrom = (source) => {
254
+ if (!controller.signal.aborted)
255
+ controller.abort(source.reason);
256
+ };
257
+ const onTurnAbort = () => abortFrom(turnSignal);
258
+ const onRequestAbort = () => abortFrom(requestSignal);
259
+ if (turnSignal.aborted)
260
+ abortFrom(turnSignal);
261
+ else
262
+ turnSignal.addEventListener('abort', onTurnAbort, { once: true });
263
+ if (requestSignal.aborted)
264
+ abortFrom(requestSignal);
265
+ else
266
+ requestSignal.addEventListener('abort', onRequestAbort, { once: true });
267
+ return {
268
+ signal: controller.signal,
269
+ dispose: () => {
270
+ turnSignal.removeEventListener('abort', onTurnAbort);
271
+ requestSignal.removeEventListener('abort', onRequestAbort);
272
+ },
273
+ };
274
+ }
276
275
  export class CanonAgent {
277
276
  options;
278
277
  runtimeConnection;
@@ -283,7 +282,6 @@ export class CanonAgent {
283
282
  sessionManager = null;
284
283
  handler = null;
285
284
  contactRequestHandler = null;
286
- contactRequestUpdatedHandler = null;
287
285
  contactApprovedHandler = null;
288
286
  contactAddedHandler = null;
289
287
  contactRemovedHandler = null;
@@ -304,8 +302,6 @@ export class CanonAgent {
304
302
  users;
305
303
  /** Conversation discovery for choosing existing sessions intentionally. */
306
304
  conversations;
307
- reachOutInFlight = new Map();
308
- contactLifecycleStore;
309
305
  agentId = null;
310
306
  agentContext = null;
311
307
  approvalManager = null;
@@ -352,9 +348,6 @@ export class CanonAgent {
352
348
  rtdbUrl: this.runtimeConnection.rtdbUrl,
353
349
  firebaseApiKey: this.runtimeConnection.firebaseWebApiKey,
354
350
  };
355
- this.contactLifecycleStore = options.ownerBoundCommunication?.enabled
356
- ? options.ownerBoundCommunication.lifecycleStore ?? new InMemoryContactLifecycleStore()
357
- : null;
358
351
  this.apiClient = new CanonClient(this.options.apiKey, this.options.baseUrl);
359
352
  this.typingSignals = createTypingStatusPublisher({
360
353
  setTyping: (conversationId, typing, status) => status
@@ -368,9 +361,6 @@ export class CanonAgent {
368
361
  list: () => apiClient.listContacts(),
369
362
  get: (contactId) => apiClient.getContact(contactId),
370
363
  remove: (contactId) => apiClient.deleteContact(contactId),
371
- request: (targetUserId, message) => apiClient.createContactRequest(targetUserId, message ?? null),
372
- listRequests: (options) => apiClient.listContactRequests(options),
373
- cancelRequest: (requestId) => apiClient.cancelContactRequest(requestId),
374
364
  };
375
365
  this.users = {
376
366
  block: (userId) => apiClient.blockUser(userId),
@@ -498,10 +488,6 @@ export class CanonAgent {
498
488
  this.contactRequestHandler = handler;
499
489
  return;
500
490
  }
501
- if (event === 'contactRequestUpdated') {
502
- this.contactRequestUpdatedHandler = handler;
503
- return;
504
- }
505
491
  if (event === 'contactApproved') {
506
492
  this.contactApprovedHandler = handler;
507
493
  return;
@@ -585,75 +571,12 @@ export class CanonAgent {
585
571
  await this.requireRuntimeStatePublisher().clearRuntimeActivity(conversationId, options);
586
572
  }
587
573
  /**
588
- * Resolve admission live for a target user (typically read off a shared
589
- * contact card) and route into either an immediate message or a contact
590
- * request. Never reads `card.accessLevel` that snapshot is stale by the
591
- * time an LLM acts on it. Instead defers to `resolveAdmission` so the
592
- * answer reflects the target's *current* inbound policy.
574
+ * Message an existing Canon conversation or start/continue a direct one.
575
+ * Policy and admission are enforced by Canon; this method deliberately
576
+ * exposes no runtime configuration or trusted source-context fields.
593
577
  */
594
- async reachOut(card, options) {
595
- const target = {
596
- targetUserId: card.userId,
597
- ...(card.userType === 'ai_agent' && card.canonContactId
598
- ? { canonContactId: card.canonContactId }
599
- : {}),
600
- };
601
- const targetKey = target.canonContactId ?? target.targetUserId;
602
- // Include the opener/request payloads in the dedupe key so two concurrent
603
- // calls with different `text`, `requestMessage`, or setup choices don't silently collapse
604
- // and lose the second caller's intended side effect.
605
- const contextualKey = options?.selfContext
606
- ? `${options.sourceConversationId ?? ''}\u0000${options.selfContext.type}\u0000${options.selfContext.context}`
607
- : '';
608
- const inFlightKey = `${targetKey}\u0000${options?.text ?? ''}\u0000${options?.requestMessage ?? ''}\u0000${JSON.stringify(options?.sessionConfig ?? null)}\u0000${JSON.stringify(options?.sessionSelection ?? null)}\u0000${contextualKey}`;
609
- const inFlight = this.reachOutInFlight.get(inFlightKey);
610
- if (inFlight)
611
- return inFlight;
612
- const promise = this.executeReachOut(target, options).finally(() => {
613
- this.reachOutInFlight.delete(inFlightKey);
614
- });
615
- this.reachOutInFlight.set(inFlightKey, promise);
616
- return promise;
617
- }
618
- async executeReachOut(target, options) {
619
- const { targetUserId } = target;
620
- if (options?.selfContext) {
621
- if (!options.sourceConversationId) {
622
- throw new Error('sourceConversationId is required for contextual reachOut');
623
- }
624
- if (!options.text) {
625
- throw new Error('text is required for contextual reachOut');
626
- }
627
- const result = await this.apiClient.sendContextualMessage({
628
- sourceConversationId: options.sourceConversationId,
629
- targetUserId,
630
- text: options.text,
631
- selfContext: options.selfContext,
632
- requestMessage: options.requestMessage ?? null,
633
- sessionConfig: options.sessionConfig ?? null,
634
- sessionSelection: options.sessionSelection,
635
- });
636
- return result.status === 'messaged'
637
- ? {
638
- status: 'messaged',
639
- conversationId: result.conversationId,
640
- messageId: result.messageId,
641
- selfContextId: result.selfContextId,
642
- created: result.created,
643
- reused: result.reused,
644
- sessionSelection: result.sessionSelection,
645
- }
646
- : result;
647
- }
648
- return reachOutToCanonContact(this.apiClient, {
649
- ...(target.canonContactId
650
- ? { canonContactId: target.canonContactId }
651
- : { targetUserId }),
652
- text: options?.text ?? null,
653
- requestMessage: options?.requestMessage ?? null,
654
- sessionConfig: options?.sessionConfig ?? null,
655
- sessionSelection: options?.sessionSelection,
656
- });
578
+ async communicate(input) {
579
+ return this.apiClient.communicate(input);
657
580
  }
658
581
  async start() {
659
582
  if (this.running)
@@ -702,7 +625,6 @@ export class CanonAgent {
702
625
  try {
703
626
  this.agentContext = await this.apiClient.getAgentMe();
704
627
  this.ensureApprovalManager(this.agentContext);
705
- await this.reconcileContactLifecycleInbox();
706
628
  }
707
629
  catch {
708
630
  console.warn('[canon-sdk] Failed to fetch agent context — owner/access info unavailable');
@@ -749,9 +671,6 @@ export class CanonAgent {
749
671
  onContactRequest: (request) => {
750
672
  void this.handleContactRequestEvent(this.contactRequestHandler, request);
751
673
  },
752
- onContactRequestUpdated: (request) => {
753
- void this.recordAndHandleContactLifecycleEvent(request);
754
- },
755
674
  onContactApproved: (request) => {
756
675
  void this.handleContactRequestEvent(this.contactApprovedHandler, request);
757
676
  },
@@ -781,26 +700,18 @@ export class CanonAgent {
781
700
  rtm.setConnectionHandlers({
782
701
  onConnected: () => {
783
702
  this.startRuntimeHeartbeat();
784
- void this.reconcileContactLifecycleInbox().catch((error) => {
785
- console.error('[canon-sdk] Contact lifecycle reconciliation failed:', error);
786
- });
787
703
  if (!this.sseConnectedLogged) {
788
704
  this.sseConnectedLogged = true;
789
705
  console.log('[canon-sdk] SSE stream connected');
790
706
  }
791
707
  },
792
- onReplayExpired: () => {
793
- void this.reconcileContactLifecycleInbox().catch((error) => {
794
- console.error('[canon-sdk] Contact lifecycle reconciliation failed:', error);
795
- });
796
- },
797
708
  onDisconnected: () => this.stopRuntimeHeartbeat(),
798
709
  });
799
710
  this.realtimeManager = rtm;
800
711
  await rtm.start();
801
712
  }
802
- async createConversation(options) {
803
- return this.apiClient.createConversation(options);
713
+ async createGroup(options) {
714
+ return this.apiClient.createGroup(options);
804
715
  }
805
716
  // ── Calls ────────────────────────────────────────────────────────────
806
717
  // These return the LiveKit room token payload; the agent brings its own
@@ -869,37 +780,6 @@ export class CanonAgent {
869
780
  console.error('[canon-sdk] Contact-request handler failed:', error instanceof Error ? error.message : error);
870
781
  }
871
782
  }
872
- async recordAndHandleContactLifecycleEvent(request) {
873
- if (this.contactLifecycleStore
874
- && request.requesterId === this.agentId
875
- && request.sourceConversationId) {
876
- await this.contactLifecycleStore.record(request);
877
- }
878
- await this.handleContactRequestEvent(this.contactRequestUpdatedHandler, request);
879
- }
880
- async reconcileContactLifecycleInbox() {
881
- if (!this.contactLifecycleStore || !this.agentId)
882
- return;
883
- const requests = await this.apiClient.listContactRequests({
884
- direction: 'outbound',
885
- includeResolved: true,
886
- limit: 100,
887
- });
888
- if (!await this.contactLifecycleStore.isBaselineComplete()) {
889
- await reconcileContactLifecycleEvents({
890
- requests,
891
- requesterId: this.agentId,
892
- record: (request) => this.contactLifecycleStore.record(request, { pending: false }),
893
- });
894
- await this.contactLifecycleStore.completeBaseline();
895
- return;
896
- }
897
- await reconcileContactLifecycleEvents({
898
- requests,
899
- requesterId: this.agentId,
900
- record: (request) => this.contactLifecycleStore.record(request),
901
- });
902
- }
903
783
  async handleContactGraphEvent(handler, payload) {
904
784
  if (!handler)
905
785
  return;
@@ -1016,13 +896,6 @@ export class CanonAgent {
1016
896
  }
1017
897
  return {
1018
898
  ...source,
1019
- admissionActions: {
1020
- blockUser: source.admissionActions?.blockUser === true,
1021
- unblockUser: source.admissionActions?.unblockUser === true,
1022
- removeContact: source.admissionActions?.removeContact === true,
1023
- requestContact: this.options.ownerBoundCommunication?.enabled === true,
1024
- reachOut: this.options.ownerBoundCommunication?.enabled === true,
1025
- },
1026
899
  supportsInterrupt: hasInterrupt,
1027
900
  supportsInputInterrupt: source.supportsInputInterrupt === false ? false : hasInterrupt,
1028
901
  commands: normalizeRuntimeCommandDescriptors(commands),
@@ -1631,9 +1504,6 @@ export class CanonAgent {
1631
1504
  for (const m of history) {
1632
1505
  m.isOwner = m.senderId === ownerId;
1633
1506
  }
1634
- for (const m of hydratedMessages) {
1635
- m.isOwner = m.senderId === ownerId;
1636
- }
1637
1507
  }
1638
1508
  const latestMessage = hydratedMessages[hydratedMessages.length - 1] ?? null;
1639
1509
  const triggeringHumanId = latestMessage?.senderType === 'human'
@@ -1642,9 +1512,6 @@ export class CanonAgent {
1642
1512
  let replyContext = latestMessage
1643
1513
  ? resolveCanonReplyContext({ message: latestMessage, messages: history })
1644
1514
  : null;
1645
- const contactLifecycleEvents = latestMessage?.isOwner && this.contactLifecycleStore
1646
- ? await this.contactLifecycleStore.take(conversationId)
1647
- : [];
1648
1515
  const resolvedActiveSelfContextId = resolveMessageActiveSelfContextId({
1649
1516
  messageId: latestMessage?.id,
1650
1517
  activeSelfContextIdByMessageId: page.activeSelfContextIdByMessageId,
@@ -1708,6 +1575,7 @@ export class CanonAgent {
1708
1575
  ownerName: '',
1709
1576
  discoverable: false,
1710
1577
  inboundPolicy: 'approval-required',
1578
+ outboundPolicy: 'approval-required',
1711
1579
  groupJoinPolicy: 'approval-required',
1712
1580
  };
1713
1581
  const provenance = latestMessage
@@ -1797,58 +1665,7 @@ export class CanonAgent {
1797
1665
  const react = (messageId, emoji) => this.apiClient.react(conversationId, messageId, emoji);
1798
1666
  const addMember = (userId) => this.apiClient.addMember(conversationId, userId);
1799
1667
  const removeMember = (userId) => this.apiClient.removeMember(conversationId, userId);
1800
- const sendContextualMessage = (target, text, options) => this.apiClient.sendContextualMessage({
1801
- sourceConversationId: conversationId,
1802
- ...target,
1803
- text,
1804
- ...options,
1805
- messageOptions: {
1806
- ...(options.messageOptions ?? {}),
1807
- metadata: {
1808
- ...(options.messageOptions?.metadata ?? {}),
1809
- turnId,
1810
- turnSemantics: 'turn_complete',
1811
- },
1812
- },
1813
- });
1814
- const reachOut = (card, options) => {
1815
- if (!this.options.ownerBoundCommunication?.enabled) {
1816
- return this.reachOut(card, {
1817
- ...(options ?? {}),
1818
- sourceConversationId: conversationId,
1819
- });
1820
- }
1821
- if (!latestMessage?.isOwner || !latestMessage.id) {
1822
- return Promise.reject(new Error('Owner-bound reachOut requires an owner-authored foreground message.'));
1823
- }
1824
- if (options?.sessionConfig != null
1825
- || options?.sessionSelection != null
1826
- || options?.selfContext != null) {
1827
- return Promise.reject(new Error('Owner-bound reachOut does not accept model-selected session configuration or hidden context.'));
1828
- }
1829
- const boundCard = replyContext?.found && replyContext.contactCard
1830
- ? replyContext.contactCard
1831
- : card;
1832
- const text = options?.text?.trim();
1833
- if (!text)
1834
- return Promise.reject(new Error('Owner-bound reachOut requires visible text.'));
1835
- return this.apiClient.sendTo({
1836
- ...(boundCard.canonContactId
1837
- ? { canonContactId: boundCard.canonContactId }
1838
- : { targetUserId: boundCard.userId }),
1839
- sourceConversationId: conversationId,
1840
- text,
1841
- ...(options?.requestMessage ? { requestMessage: options.requestMessage } : {}),
1842
- messageOptions: {
1843
- metadata: {
1844
- sourceConversationId: conversationId,
1845
- sourceMessageId: latestMessage.id,
1846
- turnId,
1847
- turnSemantics: 'turn_complete',
1848
- },
1849
- },
1850
- });
1851
- };
1668
+ const communicate = (input) => this.communicate(input);
1852
1669
  const requestApproval = async (request) => {
1853
1670
  throwIfAborted();
1854
1671
  const manager = this.ensureApprovalManager(agent);
@@ -1916,6 +1733,7 @@ export class CanonAgent {
1916
1733
  };
1917
1734
  const requestRuntimeInput = async (request) => {
1918
1735
  throwIfAborted();
1736
+ const linkedSignal = linkAbortSignals(abortController.signal, request.signal);
1919
1737
  const inputId = safeRuntimeInputId(request.inputId, request.kind);
1920
1738
  const timeoutMs = Math.max(1_000, request.timeoutMs ?? DEFAULT_RUNTIME_INPUT_TIMEOUT_MS);
1921
1739
  const expiresAtMs = Date.now() + timeoutMs;
@@ -1971,7 +1789,7 @@ export class CanonAgent {
1971
1789
  result = await this.ensureRuntimeRequestManager().request('input', conversationId, { kind: request.kind }, {
1972
1790
  requestId: inputId,
1973
1791
  expiresAt: expiresAtMs,
1974
- signal: abortController.signal,
1792
+ signal: linkedSignal.signal,
1975
1793
  });
1976
1794
  const outcome = buildRuntimeInputOutcome(inputId, result.status, {
1977
1795
  kind: request.kind,
@@ -1997,7 +1815,7 @@ export class CanonAgent {
1997
1815
  return result;
1998
1816
  }
1999
1817
  catch (error) {
2000
- if (abortController.signal.aborted || isAbortLikeError(error)) {
1818
+ if (abortController.signal.aborted || request.signal?.aborted || isAbortLikeError(error)) {
2001
1819
  if (requestCreated) {
2002
1820
  // Abort landing before the manager wired its cancel (e.g. during
2003
1821
  // the pre-request writeTurn/typing round-trips) leaves the
@@ -2020,6 +1838,81 @@ export class CanonAgent {
2020
1838
  await resumeTurnFromWaiting();
2021
1839
  return result;
2022
1840
  }
1841
+ finally {
1842
+ linkedSignal.dispose();
1843
+ }
1844
+ };
1845
+ const requestPlanReview = async (request) => {
1846
+ throwIfAborted();
1847
+ const linkedSignal = linkAbortSignals(abortController.signal, request.signal);
1848
+ const planId = request.planId && RUNTIME_INPUT_ID_PATTERN.test(request.planId)
1849
+ ? request.planId
1850
+ : randomUUID();
1851
+ const timeoutMs = Math.max(1_000, request.timeoutMs ?? DEFAULT_RUNTIME_INPUT_TIMEOUT_MS);
1852
+ const expiresAtMs = Date.now() + timeoutMs;
1853
+ const responseUserId = normalizeResponseUserId(request.responseUserId)
1854
+ ?? triggeringHumanId
1855
+ ?? normalizeResponseUserId(agent.ownerId);
1856
+ let created = false;
1857
+ let result = { status: 'timeout', planId };
1858
+ shouldPersistTurnState = true;
1859
+ try {
1860
+ result = await this.ensureRuntimeRequestManager().request('plan', conversationId, {
1861
+ ...(request.title ? { title: request.title } : {}),
1862
+ ...(request.summary ? { summary: request.summary } : {}),
1863
+ ...(request.body ? { body: request.body } : {}),
1864
+ ...(request.allowedPrompts ? { allowedPrompts: request.allowedPrompts } : {}),
1865
+ ...(responseUserId ? { responseUserId } : {}),
1866
+ turnId: request.turnId ?? turnId,
1867
+ }, {
1868
+ requestId: planId,
1869
+ expiresAt: expiresAtMs,
1870
+ responderPolicy: 'infer',
1871
+ signal: linkedSignal.signal,
1872
+ onCreated: async () => {
1873
+ created = true;
1874
+ try {
1875
+ await turnOutput.addBlock({
1876
+ id: `plan:${planId}`,
1877
+ kind: 'input',
1878
+ status: 'pending',
1879
+ title: request.title ?? 'Plan review',
1880
+ summary: request.summary ?? 'Review requested',
1881
+ });
1882
+ await turnOutput.waitingInput();
1883
+ }
1884
+ catch { }
1885
+ await writeTurn('waiting_input');
1886
+ try {
1887
+ await this.typingSignals.clear(conversationId);
1888
+ }
1889
+ catch { }
1890
+ },
1891
+ });
1892
+ throwIfAborted();
1893
+ shouldPersistTurnState = false;
1894
+ try {
1895
+ await turnOutput.completeBlock(`plan:${planId}`, {
1896
+ summary: `Plan ${result.status}`,
1897
+ });
1898
+ }
1899
+ catch { }
1900
+ await resumeTurnFromWaiting();
1901
+ return result;
1902
+ }
1903
+ catch (error) {
1904
+ if (abortController.signal.aborted || request.signal?.aborted || isAbortLikeError(error)) {
1905
+ throw error;
1906
+ }
1907
+ shouldPersistTurnState = false;
1908
+ if (!created)
1909
+ throw error;
1910
+ await resumeTurnFromWaiting();
1911
+ return result;
1912
+ }
1913
+ finally {
1914
+ linkedSignal.dispose();
1915
+ }
2023
1916
  };
2024
1917
  const sendCard = async (request) => {
2025
1918
  throwIfAborted();
@@ -2209,7 +2102,6 @@ export class CanonAgent {
2209
2102
  messages: hydratedMessages,
2210
2103
  history,
2211
2104
  replyContext,
2212
- contactLifecycleEvents,
2213
2105
  conversationId,
2214
2106
  conversation,
2215
2107
  ...(groupContext ? { groupContext } : {}),
@@ -2221,8 +2113,7 @@ export class CanonAgent {
2221
2113
  react,
2222
2114
  addMember,
2223
2115
  removeMember,
2224
- sendContextualMessage,
2225
- reachOut,
2116
+ communicate,
2226
2117
  agent,
2227
2118
  activeSelfContextId,
2228
2119
  selfContexts,
@@ -2232,6 +2123,7 @@ export class CanonAgent {
2232
2123
  turnVerbosity,
2233
2124
  requestApproval,
2234
2125
  requestRuntimeInput,
2126
+ requestPlanReview,
2235
2127
  requestCard,
2236
2128
  sendCard,
2237
2129
  abortSignal: abortController.signal,
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, ParticipationSuppressedPayload, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, 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, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimePlanRequestPayload, RuntimePlanRequestResult, SessionRule, } from '@canonmsg/core';
5
5
  export { SessionManager } from './session-manager.js';
6
6
  export { DEFAULT_ANTHROPIC_REQUEST_HEADROOM_BYTES, DEFAULT_MEDIA_CACHE_DIR, DEFAULT_MEDIA_MATERIALIZATION_BYTES, MAX_ANTHROPIC_IMAGE_RAW_BYTES, MAX_ANTHROPIC_REQUEST_BYTES, MAX_CANON_MEDIA_BYTES, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, materializeReplyContextMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, toAnthropicImageBlocksWithinBudget, uploadMediaFile, } from './media.js';
7
7
  export type { AnthropicImageBlock, AnthropicImageBudgetOptions, 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
- export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, ContactRequestUpdatedPayload, ContactRequestListOptions, ContactRequestRequirements, ContactRequestLifecyclePhase, GroupInviteRequirements, 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, ContactRequestUpdatedHandler, ContactLifecycleStore, FinalMessageResult, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ParticipationSuppressedHandler, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
10
+ export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, GroupInviteRequirements, CanonMessage, CanonConversation, CommunicateInput, CommunicateResult, DirectConversationSelection, CanonConversationsPage, CanonConversationsPageOptions, CanonReplyContext, CanonSelfContext, CanonTurnContextV2, CanonRuntimeDescriptor, MessageUpdatedPayload, SendMessageOptions, CreateGroupOptions, CreateGroupResult, TurnVerbosity, TurnVerbosityConfig, } from '@canonmsg/core';
11
+ export type { CanonAgentConnectionOptions, CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, FinalMessageResult, MessageHandler, MessageHandlerContext, MessageUpdatedHandler, ParticipationSuppressedHandler, ProgressMessageOptions, ProgressMessageResult, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimePlanReviewRequest, RuntimePlanReviewResult, 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 ContactRequestUpdatedPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload, type ParticipationSuppressedPayload, 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:
@@ -16,7 +16,6 @@ export declare class RealtimeManager {
16
16
  private suppressedSseErrorCount;
17
17
  private onAgentContext;
18
18
  private onContactRequest;
19
- private onContactRequestUpdated;
20
19
  private onContactApproved;
21
20
  private onContactAdded;
22
21
  private onContactRemoved;
@@ -26,7 +25,6 @@ export declare class RealtimeManager {
26
25
  private onMessageDeleted;
27
26
  private onConnected;
28
27
  private onDisconnected;
29
- private onReplayExpired;
30
28
  private onCallStarted;
31
29
  private onCallEnded;
32
30
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, options?: {
@@ -39,7 +37,6 @@ export declare class RealtimeManager {
39
37
  setOnAgentContext(cb: (ctx: AgentContext) => void): void;
40
38
  setContactRequestHandlers(handlers: {
41
39
  onContactRequest?: (payload: ContactRequestPayload) => void;
42
- onContactRequestUpdated?: (payload: ContactRequestUpdatedPayload) => void;
43
40
  onContactApproved?: (payload: ContactApprovedPayload) => void;
44
41
  }): void;
45
42
  setContactGraphHandlers(handlers: {
@@ -61,7 +58,6 @@ export declare class RealtimeManager {
61
58
  setConnectionHandlers(handlers: {
62
59
  onConnected?: () => void;
63
60
  onDisconnected?: () => void;
64
- onReplayExpired?: () => void;
65
61
  }): void;
66
62
  setCallHandlers(handlers: {
67
63
  onCallStarted?: (payload: VoiceSessionEventPayload) => void;
package/dist/realtime.js CHANGED
@@ -17,7 +17,6 @@ export class RealtimeManager {
17
17
  suppressedSseErrorCount = 0;
18
18
  onAgentContext = null;
19
19
  onContactRequest = null;
20
- onContactRequestUpdated = null;
21
20
  onContactApproved = null;
22
21
  onContactAdded = null;
23
22
  onContactRemoved = null;
@@ -27,7 +26,6 @@ export class RealtimeManager {
27
26
  onMessageDeleted = null;
28
27
  onConnected = null;
29
28
  onDisconnected = null;
30
- onReplayExpired = null;
31
29
  onCallStarted = null;
32
30
  onCallEnded = null;
33
31
  constructor(apiKey, debouncer, agentId, streamUrl, options) {
@@ -104,9 +102,6 @@ export class RealtimeManager {
104
102
  onContactRequest: (payload) => {
105
103
  this.onContactRequest?.(payload);
106
104
  },
107
- onContactRequestUpdated: (payload) => {
108
- this.onContactRequestUpdated?.(payload);
109
- },
110
105
  onContactApproved: (payload) => {
111
106
  this.onContactApproved?.(payload);
112
107
  },
@@ -130,7 +125,6 @@ export class RealtimeManager {
130
125
  },
131
126
  onReplayExpired: (payload) => {
132
127
  console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''}; missed history is available via explicit REST fetch`);
133
- this.onReplayExpired?.();
134
128
  },
135
129
  onError: (err) => {
136
130
  this.logSseError(err);
@@ -181,7 +175,6 @@ export class RealtimeManager {
181
175
  }
182
176
  setContactRequestHandlers(handlers) {
183
177
  this.onContactRequest = handlers.onContactRequest ?? null;
184
- this.onContactRequestUpdated = handlers.onContactRequestUpdated ?? null;
185
178
  this.onContactApproved = handlers.onContactApproved ?? null;
186
179
  }
187
180
  setContactGraphHandlers(handlers) {
@@ -208,7 +201,6 @@ export class RealtimeManager {
208
201
  setConnectionHandlers(handlers) {
209
202
  this.onConnected = handlers.onConnected ?? null;
210
203
  this.onDisconnected = handlers.onDisconnected ?? null;
211
- this.onReplayExpired = handlers.onReplayExpired ?? null;
212
204
  }
213
205
  setCallHandlers(handlers) {
214
206
  this.onCallStarted = handlers.onCallStarted ?? null;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { AddMemberResult, ContactRequestListOptions, ContactRequestRequirements, ContactRequestLifecyclePhase, GroupInviteRequirements, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, CanonMessage, CanonConversation, CanonReplyContext, CanonContact, CanonContactRequest, ContactRequestUpdatedPayload, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, CreateConversationResult, DirectSessionSelection, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, VerbSessionConfig, CreateConversationOptions, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, SessionRule, ApprovalResult, } from '@canonmsg/core';
2
- import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CanonReplyContext, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, SendMessageOptions, SendContextualSelfContextInput, VerbSessionConfig, DirectSessionSelection, TurnOutputBlock, TurnOutputBlockInput } from '@canonmsg/core';
1
+ export type { AddMemberResult, GroupInviteRequirements, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, CanonMessage, CanonConversation, CommunicateInput, CommunicateResult, CanonReplyContext, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolveAdmissionTargetInput, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, CreateGroupOptions, CreateGroupResult, SendMessageOptions, SessionConfig, TurnLifecycleState, TurnOutputBlock, TurnOutputBlockInput, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRequestMetadata, ApprovalRisk, ApprovalReplyMetadata, ApprovalOutcomeMetadata, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimePlanRequestPayload, RuntimePlanRequestResult, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, MediaAttachment, VideoProcessingStatus, VideoProcessingErrorCode, SessionRule, ApprovalResult, } from '@canonmsg/core';
2
+ import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, CommunicateInput, CommunicateResult, CanonReplyContext, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, CanonRuntimeProvenance, CanonTurnContextV2, ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalResult, ApprovalRisk, RuntimeInputChoice, RuntimeInputAnswers, RuntimeInputKind, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimePlanRequestPayload, RuntimePlanRequestResult, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, SendMessageOptions, 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
  /**
@@ -119,6 +119,8 @@ export interface RuntimeInputRequest {
119
119
  native?: RuntimeInputNativeMetadata;
120
120
  inputId?: string;
121
121
  timeoutMs?: number;
122
+ /** Aborts the pending input request without waiting for the enclosing turn to stop. */
123
+ signal?: AbortSignal;
122
124
  /** Human conversation member who should answer. Secret and sudo prompts remain owner-only. */
123
125
  responseUserId?: string;
124
126
  }
@@ -128,6 +130,14 @@ export interface RuntimeInputResult {
128
130
  answers?: RuntimeInputAnswers;
129
131
  inputId: string;
130
132
  }
133
+ /** Native Canon plan-review card backed by Core's existing runtime-plan interaction. */
134
+ export interface RuntimePlanReviewRequest extends RuntimePlanRequestPayload {
135
+ planId?: string;
136
+ timeoutMs?: number;
137
+ /** Aborts the pending plan review without waiting for the enclosing turn to stop. */
138
+ signal?: AbortSignal;
139
+ }
140
+ export type RuntimePlanReviewResult = RuntimePlanRequestResult;
131
141
  export interface RuntimeCardRequest {
132
142
  card: RuntimeCardV1;
133
143
  cardId?: string;
@@ -152,8 +162,6 @@ export interface MessageHandlerContext {
152
162
  history: CanonMessage[];
153
163
  /** Resolved message/media content for the latest swipe-reply target, if any. */
154
164
  replyContext: CanonReplyContext | null;
155
- /** Terminal introduction events retained until this natural owner turn. */
156
- contactLifecycleEvents: import('@canonmsg/core').CanonContactRequest[];
157
165
  conversationId: string;
158
166
  conversation: CanonConversation;
159
167
  /** Lightweight group awareness, present for group conversations. */
@@ -178,14 +186,8 @@ export interface MessageHandlerContext {
178
186
  addMember: (userId: string) => Promise<AddMemberResult>;
179
187
  /** Remove a member from this conversation (requires owner/admin role) */
180
188
  removeMember: (userId: string) => Promise<void>;
181
- /** Send into another Canon conversation with private cross-session self-context. */
182
- sendContextualMessage: (target: {
183
- targetConversationId: string;
184
- } | {
185
- targetUserId: string;
186
- }, text: string, options: Omit<import('@canonmsg/core').SendContextualMessageOptions, 'sourceConversationId' | 'targetConversationId' | 'targetUserId' | 'text'>) => Promise<import('@canonmsg/core').SendContextualMessageResult>;
187
- /** Reach a contact card from this conversation; contextual reach-outs use this conversation as source. */
188
- reachOut: (card: ContactCardPayload, options?: Omit<ReachOutOptions, 'sourceConversationId'>) => Promise<ReachOutResult>;
189
+ /** Message an existing Canon conversation or start/continue a direct one. */
190
+ communicate: (input: CommunicateInput) => Promise<CommunicateResult>;
189
191
  /** Trusted agent identity & access context */
190
192
  agent: import('@canonmsg/core').AgentContext;
191
193
  /** Active private self-context to continue for this turn, if Canon supplied one. */
@@ -218,6 +220,12 @@ export interface MessageHandlerContext {
218
220
  * are never persisted in Canon message metadata.
219
221
  */
220
222
  requestRuntimeInput: (request: RuntimeInputRequest) => Promise<RuntimeInputResult>;
223
+ /**
224
+ * Present a native Canon plan-review card and wait for approve, revise,
225
+ * reject, cancellation, or timeout. Canon owns presentation and response
226
+ * routing; the runtime remains responsible for enforcing the decision.
227
+ */
228
+ requestPlanReview: (request: RuntimePlanReviewRequest) => Promise<RuntimePlanReviewResult>;
221
229
  /**
222
230
  * Ask the triggering human to review/respond to a generic rich card. The
223
231
  * visible card document is redacted for presentation; raw response values
@@ -312,15 +320,6 @@ export interface CanonAgentOptions extends CanonAgentConnectionOptions {
312
320
  clientType?: import('@canonmsg/core').AgentClientType;
313
321
  /** Optional runtime descriptor published to Canon for setup/live UI rendering. */
314
322
  runtimeDescriptor?: import('@canonmsg/core').CanonRuntimeDescriptor;
315
- /**
316
- * Opt into the owner-authorized introduction surface advertised to Canon.
317
- * Lifecycle events never start a model turn; they are passed to the next
318
- * natural owner handler invocation and to onContactRequestUpdated.
319
- */
320
- ownerBoundCommunication?: {
321
- enabled: true;
322
- lifecycleStore?: ContactLifecycleStore;
323
- };
324
323
  /** Optional Canon runtime signal handlers. Enables interrupt controls when provided. */
325
324
  runtimeControls?: RuntimeControlHandlers;
326
325
  /** Runtime publishing surface. Use `host` when this agent owns live runtime controls. */
@@ -347,15 +346,6 @@ export interface CanonAgentOptions extends CanonAgentConnectionOptions {
347
346
  turnVerbosity?: import('./turn-verbosity-option.js').CanonAgentTurnVerbosityOption;
348
347
  }
349
348
  export type ContactRequestHandler = (request: import('@canonmsg/core').CanonContactRequest) => void | Promise<void>;
350
- export type ContactRequestUpdatedHandler = ContactRequestHandler;
351
- export interface ContactLifecycleStore {
352
- record(request: import('@canonmsg/core').CanonContactRequest, options?: {
353
- pending?: boolean;
354
- }): boolean | Promise<boolean>;
355
- take(sourceConversationId: string): import('@canonmsg/core').CanonContactRequest[] | Promise<import('@canonmsg/core').CanonContactRequest[]>;
356
- isBaselineComplete(): boolean | Promise<boolean>;
357
- completeBaseline(): void | Promise<void>;
358
- }
359
349
  export type ContactAddedHandler = (contact: import('@canonmsg/core').ContactAddedPayload) => void | Promise<void>;
360
350
  export type ContactRemovedHandler = (payload: import('@canonmsg/core').ContactRemovedPayload) => void | Promise<void>;
361
351
  /**
@@ -365,56 +355,3 @@ export type ContactRemovedHandler = (payload: import('@canonmsg/core').ContactRe
365
355
  * benched agent can tell deliberate participation policy from a dead stream.
366
356
  */
367
357
  export type ParticipationSuppressedHandler = (payload: import('@canonmsg/core').ParticipationSuppressedPayload) => void | Promise<void>;
368
- /**
369
- * Result of `agent.reachOut(card)` — describes which side-effect ran so the
370
- * caller can decide what to tell the LLM. `messaged` means the agent opened
371
- * (or sent into) a direct conversation; `requested` means the target's
372
- * inbound policy required a contact request, which has been created;
373
- * `pending` means a prior outbound request is still awaiting approval; and
374
- * `blocked` / `unavailable` describe terminal states with `reason` set.
375
- */
376
- export type ReachOutResult = {
377
- status: 'messaged';
378
- conversationId: string;
379
- messageId?: string;
380
- selfContextId?: string;
381
- created?: boolean;
382
- reused?: boolean;
383
- sessionSelection?: DirectSessionSelection['mode'];
384
- } | {
385
- status: 'requested';
386
- requestId: string | null;
387
- deferredIntentId?: string | null;
388
- } | {
389
- status: 'pending';
390
- requestId: string | null;
391
- deferredIntentId?: string | null;
392
- } | {
393
- status: 'setup_required';
394
- reason: string;
395
- } | {
396
- status: 'no_session';
397
- reason: string;
398
- } | {
399
- status: 'blocked' | 'unavailable';
400
- reason: string;
401
- };
402
- export interface ReachOutOptions {
403
- /**
404
- * Optional first message; sent now when allowed or parked for exact delivery
405
- * after approval when it is visible text <= 4 KiB. Attachments and hidden
406
- * session setup cannot be parked; a setup-requiring coding target returns
407
- * `setup_required` before a contact request is created.
408
- */
409
- text?: string;
410
- /** Optional contact-request note. Defaults to `text` when admission is `request-required`. */
411
- requestMessage?: string;
412
- /** Explicit session setup to use when the contact-card target is an agent. */
413
- sessionConfig?: VerbSessionConfig | null;
414
- /** Whether to continue an existing direct agent session or start a fresh one. */
415
- sessionSelection?: DirectSessionSelection;
416
- /** Source conversation for contextual cross-session reach-outs. */
417
- sourceConversationId?: string;
418
- /** Private context for the agent when this reach-out sends a cross-session message. */
419
- selfContext?: SendContextualSelfContextInput;
420
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "8.9.0",
3
+ "version": "9.1.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.7.0"
31
+ "@canonmsg/core": "^11.0.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"