@canonmsg/agent-sdk 9.0.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
@@ -200,6 +200,7 @@ The `message` event handler receives a context object with:
200
200
  | `turnVerbosity` | `'verbose' \| 'quiet'` | Resolved emission mode for this turn — see [Turn verbosity](#turn-verbosity). Fixed for the whole turn |
201
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 |
202
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 |
203
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 |
204
205
  | `media` | `{ materialize, uploadFile, replyWithFile }` | Canon-managed access to real media bytes via `~/.canon/media-cache` plus local-file uploads back into Canon |
205
206
  | `session` | `SessionInfo \| undefined` | Per-conversation queue/session state when sessions are enabled |
@@ -246,7 +247,7 @@ Reaction update events are interaction state, not new chat turns. They do not ca
246
247
 
247
248
  ### Human-in-the-loop cards
248
249
 
249
- 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.
250
251
 
251
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.
252
253
 
@@ -245,6 +245,33 @@ function isAbortLikeError(error) {
245
245
  return true;
246
246
  return typeof record.message === 'string' && /\babort(?:ed)?\b/i.test(record.message);
247
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
+ }
248
275
  export class CanonAgent {
249
276
  options;
250
277
  runtimeConnection;
@@ -1706,6 +1733,7 @@ export class CanonAgent {
1706
1733
  };
1707
1734
  const requestRuntimeInput = async (request) => {
1708
1735
  throwIfAborted();
1736
+ const linkedSignal = linkAbortSignals(abortController.signal, request.signal);
1709
1737
  const inputId = safeRuntimeInputId(request.inputId, request.kind);
1710
1738
  const timeoutMs = Math.max(1_000, request.timeoutMs ?? DEFAULT_RUNTIME_INPUT_TIMEOUT_MS);
1711
1739
  const expiresAtMs = Date.now() + timeoutMs;
@@ -1761,7 +1789,7 @@ export class CanonAgent {
1761
1789
  result = await this.ensureRuntimeRequestManager().request('input', conversationId, { kind: request.kind }, {
1762
1790
  requestId: inputId,
1763
1791
  expiresAt: expiresAtMs,
1764
- signal: abortController.signal,
1792
+ signal: linkedSignal.signal,
1765
1793
  });
1766
1794
  const outcome = buildRuntimeInputOutcome(inputId, result.status, {
1767
1795
  kind: request.kind,
@@ -1787,7 +1815,7 @@ export class CanonAgent {
1787
1815
  return result;
1788
1816
  }
1789
1817
  catch (error) {
1790
- if (abortController.signal.aborted || isAbortLikeError(error)) {
1818
+ if (abortController.signal.aborted || request.signal?.aborted || isAbortLikeError(error)) {
1791
1819
  if (requestCreated) {
1792
1820
  // Abort landing before the manager wired its cancel (e.g. during
1793
1821
  // the pre-request writeTurn/typing round-trips) leaves the
@@ -1810,6 +1838,81 @@ export class CanonAgent {
1810
1838
  await resumeTurnFromWaiting();
1811
1839
  return result;
1812
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
+ }
1813
1916
  };
1814
1917
  const sendCard = async (request) => {
1815
1918
  throwIfAborted();
@@ -2020,6 +2123,7 @@ export class CanonAgent {
2020
2123
  turnVerbosity,
2021
2124
  requestApproval,
2022
2125
  requestRuntimeInput,
2126
+ requestPlanReview,
2023
2127
  requestCard,
2024
2128
  sendCard,
2025
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
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, 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, RuntimeApprovalRequest, RuntimeInputRequest, RuntimeInputResult, RuntimePlanReviewRequest, RuntimePlanReviewResult, RuntimeControlSurface, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
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, 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, RuntimeCardNativeMetadata, RuntimeCardV1, ResumableMediaUploadResult, SendMessageOptions, 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;
@@ -210,6 +220,12 @@ export interface MessageHandlerContext {
210
220
  * are never persisted in Canon message metadata.
211
221
  */
212
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>;
213
229
  /**
214
230
  * Ask the triggering human to review/respond to a generic rich card. The
215
231
  * visible card document is redacted for presentation; raw response values
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "9.0.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",