@canonmsg/agent-sdk 13.0.3 → 13.2.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/dist/cli.js CHANGED
@@ -17,13 +17,15 @@ import {
17
17
  loadInstructions,
18
18
  loadMlsDevConfiguration,
19
19
  loadRelayConfiguration,
20
+ loadWakeConfiguration,
21
+ parseIdleTimeout,
20
22
  queueErrand,
21
23
  readRelayCredential,
22
24
  relayRunnerHome,
23
25
  runMlsDevSetup,
24
26
  startRelayChannel,
25
27
  writeRelayCredential
26
- } from "./chunks/chunk-XKUG2BQR.js";
28
+ } from "./chunks/chunk-6FD6KFQN.js";
27
29
  import {
28
30
  assertPrivateHome,
29
31
  runnerLockName
@@ -38,7 +40,7 @@ import {
38
40
  resolveCanonRuntimeConnection,
39
41
  runCli,
40
42
  verifyCanonRuntimeConnection
41
- } from "./chunks/chunk-ZPBOGOAD.js";
43
+ } from "./chunks/chunk-5YKGDUJX.js";
42
44
  import "./chunks/chunk-YJUPKU47.js";
43
45
  import "./chunks/chunk-YHDDOADK.js";
44
46
  import {
@@ -56,10 +58,10 @@ var RUNNER_VERSION = readCliPackageVersion(import.meta.url);
56
58
  var RUNNER_CLIENT = isCanonClientVersion(RUNNER_VERSION) ? { kind: "runner", version: RUNNER_VERSION } : void 0;
57
59
  function once(handler) {
58
60
  let called = false;
59
- return (error) => {
61
+ return (...args) => {
60
62
  if (!called) {
61
63
  called = true;
62
- handler(error);
64
+ handler(...args);
63
65
  }
64
66
  };
65
67
  }
@@ -97,6 +99,14 @@ command also takes --mls-dev-config <path>, an operator's own custody instead. W
97
99
  of these, the agent refuses every room, and where Canon encrypts every new conversation
98
100
  it does not start.
99
101
 
102
+ Waking and sleeping: code, claude, codex and acp also take [--wake-config <path> |
103
+ --wake-off] [--idle-timeout <duration>]. With --wake-config the agent registers, at
104
+ each start, a public HTTPS callback for up to 32 rooms; Canon POSTs it a signed,
105
+ content-free hint when a person types or a message arrives there, so the host can
106
+ start (or resume) the agent. With --idle-timeout the agent stops and exits 0 once it
107
+ has had nothing to do for that long: no turn, no input, no errand, nothing Canon has
108
+ yet to accept. Nothing is lost while it is stopped; it answers once it starts again.
109
+
100
110
  Team rooms (an owner's agents working together): claude, codex and acp also take
101
111
  [--instructions <file>] [--trust-agent <agentId>]...; every run command takes
102
112
  [--agent-turn-cap <n>] [--mention-alias <name>]....
@@ -174,6 +184,17 @@ Flags:
174
184
  $CANON_HOME/relay. The relay serves only encrypted rooms,
175
185
  which are then not end-to-end encrypted to that agent. It sets
176
186
  up the relay's device with that credential.
187
+ --wake-config <path> Register the agent's wake callback at start: a private JSON file
188
+ (0600, owned by you, not a link) {"version": 1, "callbackUrl":
189
+ "https://...", "secret": "whsec_<base64 of 32 random bytes>",
190
+ "conversationIds": [up to 32 room ids]}. Canon checks the URL
191
+ with a signed challenge first; the agent does not start if
192
+ that fails. Without the flag a registration is left as it is.
193
+ --wake-off Remove the agent's wake callback at start.
194
+ --idle-timeout <d> Stop and exit 0 after this long with nothing to do: whole
195
+ seconds, or 90s, 10m, 1h. Typing in a room does not reach the
196
+ agent: a host that also receives typing hints decides itself
197
+ whether to keep the agent awake for them.
177
198
  --mls-dev-config <p> An operator's own MLS configuration, instead of the device the
178
199
  runner keeps itself: an absolute path to a private JSON file
179
200
  with the environment's issuer pins and the custody helper (no
@@ -263,6 +284,16 @@ async function main() {
263
284
  if (values["guest-mode"] !== void 0 && values["unguarded-guests"]) throw new UsageError("--guest-mode and --unguarded-guests are alternatives.");
264
285
  const team = teamOptions(values);
265
286
  if (values.driver === "connector" && values["allow-session-for-destructive"]) throw new UsageError("--allow-session-for-destructive goes with a driver that asks.");
287
+ if (values.driver === "connector" && (values["wake-config"] !== void 0 || values["wake-off"] || values["idle-timeout"] !== void 0)) {
288
+ throw new UsageError("--wake-config, --wake-off and --idle-timeout go with an agent's own key, not --driver connector.");
289
+ }
290
+ if (values["wake-config"] !== void 0 && values["wake-off"]) throw new UsageError("--wake-config and --wake-off are alternatives.");
291
+ let idleTimeoutMs;
292
+ try {
293
+ idleTimeoutMs = values["idle-timeout"] !== void 0 ? parseIdleTimeout(values["idle-timeout"]) : void 0;
294
+ } catch (error) {
295
+ throw new UsageError(error.message);
296
+ }
266
297
  if (values.driver === "connector") return runRelay(values, team);
267
298
  const permissionMode = values["permission-mode"];
268
299
  if (permissionMode !== void 0 && !PERMISSION_MODES.includes(permissionMode)) {
@@ -278,6 +309,7 @@ async function main() {
278
309
  const trustedAgents = values["trust-agent"] ?? [];
279
310
  if (trustedAgents.some((id) => !/^[\w.:-]{1,200}$/.test(id))) throw new UsageError("--trust-agent takes an agent id.");
280
311
  const instructions = values.instructions !== void 0 ? await loadInstructions(resolve(values.instructions)) : void 0;
312
+ const wake = values["wake-off"] ? null : values["wake-config"] !== void 0 ? await loadWakeConfiguration(resolve(values["wake-config"])) : void 0;
281
313
  const { environmentId, home, profile, apiKey } = await settings(values);
282
314
  const mls = await mlsDevice(values, environmentId, home);
283
315
  const baseUrl = process.env.CANON_BASE_URL || profile?.baseUrl;
@@ -335,6 +367,15 @@ async function main() {
335
367
  ...trustedAgents.length ? { trustedAgents } : {},
336
368
  ...values["allow-session-for-destructive"] ? { sessionApprovalForDestructive: true } : {},
337
369
  ...RUNNER_CLIENT ? { client: RUNNER_CLIENT } : {},
370
+ ...wake !== void 0 ? { wake } : {},
371
+ // Idle: a clean stop and exit 0, so a supervisor that restarts only on failure leaves the agent stopped until it is woken.
372
+ ...idleTimeoutMs !== void 0 ? { idleTimeoutMs, onIdle: once(() => {
373
+ console.error(`canon-agent: nothing to do for ${values["idle-timeout"]}; stopping.`);
374
+ runner.stop().then(() => process.exit(0), (error) => {
375
+ console.error(`canon-agent: ${error instanceof Error ? error.message : String(error)}`);
376
+ process.exit(1);
377
+ });
378
+ }) } : {},
338
379
  onFatal: once((error) => {
339
380
  console.error(`canon-agent: ${error.message}`);
340
381
  void runner.stop().catch(() => void 0).finally(() => process.exit(1));
@@ -347,7 +388,7 @@ async function main() {
347
388
  await runner.stop().catch(() => void 0);
348
389
  throw error;
349
390
  }
350
- console.error(`canon-agent: running the ${values.driver} driver in ${environmentId}. Ctrl-C stops it.`);
391
+ console.error(`canon-agent: running the ${values.driver} driver in ${environmentId}. Ctrl-C stops it${idleTimeoutMs !== void 0 ? `, and it stops itself after ${values["idle-timeout"]} with nothing to do` : ""}.`);
351
392
  }
352
393
  __name(main, "main");
353
394
  async function runRelay(values, team) {
@@ -478,7 +519,10 @@ function parse() {
478
519
  why: { type: "string" },
479
520
  origin: { type: "string" },
480
521
  follow: { type: "boolean" },
481
- cancel: { type: "string" }
522
+ cancel: { type: "string" },
523
+ "wake-config": { type: "string" },
524
+ "wake-off": { type: "boolean" },
525
+ "idle-timeout": { type: "string" }
482
526
  }
483
527
  });
484
528
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createRunner, runtimeDescriptor } from './runner/runner.js';
2
- export type { Runner, RunnerOptions } from './runner/runner.js';
2
+ export type { Runner, RunnerActivity, RunnerOptions } from './runner/runner.js';
3
+ export { loadWakeConfiguration, parseWakeConfiguration } from './runner/wake.js';
3
4
  export { ConnectorRelayError, DriverSessionError } from './runner/types.js';
4
5
  export { loadMlsDevConfiguration, MLS_SETUP_ACTIONS, mlsDevRoomPackageId, runMlsDevSetup } from './runner/mls.js';
5
6
  export type { MlsSetupAction, RunnerMlsOptions } from './runner/mls.js';
package/dist/index.js CHANGED
@@ -18,13 +18,15 @@ import {
18
18
  loadClaudeQuery,
19
19
  loadMlsDevConfiguration,
20
20
  loadRelayConfiguration,
21
+ loadWakeConfiguration,
21
22
  managedMlsDevice,
22
23
  mlsDevRoomPackageId,
24
+ parseWakeConfiguration,
23
25
  queueErrand,
24
26
  runMlsDevSetup,
25
27
  runtimeDescriptor,
26
28
  startRelayChannel
27
- } from "./chunks/chunk-XKUG2BQR.js";
29
+ } from "./chunks/chunk-6FD6KFQN.js";
28
30
  import "./chunks/chunk-DTAWEQCM.js";
29
31
  import {
30
32
  CanonApiError,
@@ -50,7 +52,7 @@ import {
50
52
  toAnthropicImageBlock,
51
53
  toAnthropicImageBlocksWithinBudget,
52
54
  uploadMediaFile
53
- } from "./chunks/chunk-ZPBOGOAD.js";
55
+ } from "./chunks/chunk-5YKGDUJX.js";
54
56
  import "./chunks/chunk-YJUPKU47.js";
55
57
  import "./chunks/chunk-YHDDOADK.js";
56
58
  import "./chunks/chunk-5WPAB5MB.js";
@@ -88,11 +90,13 @@ export {
88
90
  loadClaudeQuery,
89
91
  loadMlsDevConfiguration,
90
92
  loadRelayConfiguration,
93
+ loadWakeConfiguration,
91
94
  managedMlsDevice,
92
95
  materializeAttachment,
93
96
  materializeMessageMedia,
94
97
  materializeReplyContextMedia,
95
98
  mlsDevRoomPackageId,
99
+ parseWakeConfiguration,
96
100
  queueErrand,
97
101
  resolveAttachmentMimeType,
98
102
  resumeRegistration,
@@ -1,11 +1,6 @@
1
1
  import type { AgentWakeConfiguration } from '../backend-contracts/index.js';
2
- import { type CanonMessage, type CanonConversation, type CanonUnifiedDiff, type CanonMessagesPage, type AgentContext, type MediaAttachment, type SendMessageOptions, type RegistrationStatus, type AgentOriginDeclaration, type SetRuntimeTurnOptions, type SetStreamingOptions } from './types.js';
3
- import type { RuntimeInputKind } from './runtime-cards.js';
4
- import type { ApprovalNativeRequestMetadata, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, SessionRule } from './approval-types.js';
5
- import type { RuntimeInputAnswers, RuntimeInputChoice, RuntimeInputNativeMetadata, RuntimeInputQuestion, RuntimeCardNativeMetadata, RuntimeCardV1 } from './runtime-cards.js';
6
- import type { InboundDisposition } from './turn-protocol.js';
2
+ import { type CanonMessage, type CanonConversation, type CanonMessagesPage, type AgentContext, type MediaAttachment, type SendMessageOptions, type RegistrationStatus, type AgentOriginDeclaration, type SetRuntimeTurnOptions } from './types.js';
7
3
  import { type CanonVerbWireRequest, type CanonVerbWireResponse, type CommunicateInput, type CommunicateResult, type DiscoverAgentsInput, type DiscoverAgentsResult } from '../backend-contracts/index.js';
8
- import { type EndpointOperation } from '../endpoint/index.js';
9
4
  import { type CanonClientEndpointOptions, type CanonEndpoint } from './endpoint-client.js';
10
5
  export interface ResumableMediaUploadResult {
11
6
  uploadId: string;
@@ -53,28 +48,6 @@ export declare class CanonClient {
53
48
  }): Promise<{
54
49
  messageId: string;
55
50
  }>;
56
- /** Inspect a trusted reply's durable delivery without rebuilding its body. */
57
- getMessageOperation(conversationId: string, messageId: string, delivery?: {
58
- operationId?: string;
59
- }): Promise<EndpointOperation | undefined>;
60
- /** Core's chunker records all ordered message intents before publishing any part. */
61
- prepareMessageGroup(conversationId: string, messageId: string, parts: Array<{
62
- text: string;
63
- options: SendMessageOptions;
64
- }>, delivery?: {
65
- operationId?: string;
66
- signal?: AbortSignal;
67
- replacesOperationId?: string;
68
- }): Promise<{
69
- id: string;
70
- sendPart(index: number, options?: {
71
- signal?: AbortSignal;
72
- timeoutMs?: number;
73
- }): Promise<{
74
- messageId: string;
75
- }>;
76
- }>;
77
- cancelMessageGroup(id: string): Promise<void>;
78
51
  /**
79
52
  * Execute a canonical verb over the wire contract (canon.verb-wire.v1).
80
53
  * Bindings build the request with projectVerbIntentToWire; the endpoint
@@ -99,13 +72,10 @@ export declare class CanonClient {
99
72
  private startWithheld;
100
73
  blockUser(userId: string): Promise<void>;
101
74
  deleteMessage(conversationId: string, messageId: string): Promise<void>;
102
- updateMessageDisposition(conversationId: string, messageId: string, inboundDisposition: InboundDisposition): Promise<void>;
103
75
  markAsRead(conversationId: string): Promise<void>;
104
76
  react(conversationId: string, messageId: string, emoji: string): Promise<void>;
105
- setStreaming(options: SetStreamingOptions): Promise<void>;
106
77
  clearStreaming(conversationId: string): Promise<void>;
107
78
  setRuntimeTurn(options: SetRuntimeTurnOptions): Promise<void>;
108
- setTyping(conversationId: string, typing: boolean, status?: 'thinking' | 'typing'): Promise<void>;
109
79
  peekRuntimeSignal(conversationId: string): Promise<Record<string, unknown>>;
110
80
  /** Acknowledge exactly the signal observed before the native handler ran. */
111
81
  acknowledgeRuntimeSignal(conversationId: string, signalId: string): Promise<Record<string, unknown>>;
@@ -124,190 +94,6 @@ export declare class CanonClient {
124
94
  mode: 'clear';
125
95
  }): Promise<void>;
126
96
  patchRuntimeStreaming(conversationId: string, patch: Record<string, unknown>): Promise<void>;
127
- createRuntimeInputRequest(options: {
128
- conversationId: string;
129
- inputId: string;
130
- kind: RuntimeInputKind;
131
- expiresAt: number;
132
- title?: string;
133
- prompt?: string;
134
- choices?: RuntimeInputChoice[];
135
- questions?: RuntimeInputQuestion[];
136
- secretName?: string;
137
- native?: RuntimeInputNativeMetadata;
138
- sensitive?: boolean;
139
- responseUserId?: string;
140
- turnId?: string;
141
- }): Promise<{
142
- success: true;
143
- inputId: string;
144
- expiresAt: number;
145
- messageId?: string;
146
- }>;
147
- consumeRuntimeInputResponse(options: {
148
- conversationId: string;
149
- inputId: string;
150
- cancel?: boolean;
151
- }): Promise<{
152
- status: 'pending';
153
- inputId: string;
154
- expiresAt?: number;
155
- } | {
156
- status: 'submitted';
157
- inputId: string;
158
- kind: RuntimeInputKind;
159
- value: string;
160
- answers?: RuntimeInputAnswers;
161
- } | {
162
- status: 'cancelled';
163
- inputId: string;
164
- kind: RuntimeInputKind;
165
- } | {
166
- status: 'timeout';
167
- inputId: string;
168
- kind: RuntimeInputKind;
169
- }>;
170
- createRuntimeApprovalRequest(options: {
171
- conversationId: string;
172
- approvalId?: string;
173
- toolName: string;
174
- toolSummary: string;
175
- expiresAt: number;
176
- responseUserId?: string;
177
- riskLevel?: 'normal' | 'destructive';
178
- risk?: ApprovalRisk;
179
- category?: ApprovalRequestCategory;
180
- runtimeId?: string;
181
- turnId?: string;
182
- native?: ApprovalNativeRequestMetadata;
183
- details?: ApprovalRequestDetail[];
184
- /** File-change preview rendered inline on the approval card. Truncate via truncateUnifiedDiff before sending. */
185
- diff?: CanonUnifiedDiff;
186
- allowSessionRule?: boolean;
187
- }): Promise<{
188
- success: true;
189
- approvalId: string;
190
- expiresAt: number;
191
- messageId?: string;
192
- responseUserId: string;
193
- allowSessionRule: boolean;
194
- }>;
195
- consumeRuntimeApprovalResponse(options: {
196
- conversationId: string;
197
- approvalId: string;
198
- cancel?: boolean;
199
- }): Promise<{
200
- status: 'pending';
201
- approvalId: string;
202
- expiresAt?: number;
203
- } | {
204
- status: 'allow';
205
- approvalId: string;
206
- sessionRule?: SessionRule;
207
- respondedBy?: string;
208
- } | {
209
- status: 'deny';
210
- approvalId: string;
211
- sessionRule?: SessionRule;
212
- respondedBy?: string;
213
- } | {
214
- status: 'timeout';
215
- approvalId: string;
216
- }>;
217
- createRuntimeCardRequest(options: {
218
- conversationId: string;
219
- card: RuntimeCardV1;
220
- cardId?: string;
221
- expiresAt?: number;
222
- responseUserId?: string;
223
- runtimeId?: string;
224
- turnId?: string;
225
- native?: RuntimeCardNativeMetadata;
226
- }): Promise<{
227
- success: true;
228
- cardId: string;
229
- messageId: string;
230
- /** True when the card has actions and a response is pending; false for display cards. */
231
- interactive: boolean;
232
- /** Effective human responder selected by Canon. */
233
- responseUserId?: string;
234
- /** Present only for interactive cards. */
235
- expiresAt?: number;
236
- }>;
237
- consumeRuntimeCardResponse(options: {
238
- conversationId: string;
239
- cardId: string;
240
- cancel?: boolean;
241
- }): Promise<{
242
- status: 'pending';
243
- cardId: string;
244
- expiresAt?: number;
245
- } | {
246
- status: 'submitted';
247
- cardId: string;
248
- actionId?: string;
249
- values?: Record<string, unknown>;
250
- respondedBy?: string;
251
- } | {
252
- status: 'cancelled';
253
- cardId: string;
254
- respondedBy?: string;
255
- } | {
256
- status: 'timeout';
257
- cardId: string;
258
- }>;
259
- /**
260
- * Create a server-owned plan-approval interaction.
261
- *
262
- * Seeds the pending node + attention + runtime-card state and authors the
263
- * `plan_approval` card message, mirroring the other `createRuntime*Request`
264
- * spine methods. Hosts resolve it through `consumeRuntimePlanResponse()` so
265
- * the canonical pending state, rather than a timeline receipt, is authoritative.
266
- */
267
- createRuntimePlanRequest(options: {
268
- conversationId: string;
269
- planId?: string;
270
- title?: string;
271
- summary?: string;
272
- body?: string;
273
- allowedPrompts?: ReadonlyArray<{
274
- tool: string;
275
- prompt: string;
276
- }>;
277
- responseUserId?: string;
278
- turnId?: string;
279
- expiresAt?: number;
280
- }): Promise<{
281
- success: true;
282
- planId: string;
283
- expiresAt: number;
284
- messageId?: string;
285
- interactive: boolean;
286
- }>;
287
- consumeRuntimePlanResponse(options: {
288
- conversationId: string;
289
- planId: string;
290
- cancel?: boolean;
291
- }): Promise<{
292
- status: 'pending';
293
- planId: string;
294
- expiresAt?: number;
295
- } | {
296
- status: 'approve' | 'revise' | 'reject';
297
- planId: string;
298
- feedback?: string;
299
- grantedPrompts?: Array<{
300
- tool: string;
301
- prompt: string;
302
- }>;
303
- receiptId?: string;
304
- } | {
305
- status: 'cancelled';
306
- planId: string;
307
- } | {
308
- status: 'timeout';
309
- planId: string;
310
- }>;
311
97
  updateRuntimeStatus(options: {
312
98
  runtime: string;
313
99
  hostMode?: boolean;
@@ -57,4 +57,3 @@ export { CANON_VERB_NAMES, CANON_VERB_NAMESPACE, CANON_VERBS_SCHEMA_ID, CANON_VE
57
57
  export { DEFAULT_DISCOVER_AGENTS_LIMIT, MAX_DISCOVER_AGENTS_LIMIT, MAX_DISCOVER_AGENTS_QUERY_LENGTH, type CanonAgentDirectoryEntry, type DiscoverAgentsInput, type DiscoverAgentsResult, } from '../backend-contracts/index.js';
58
58
  export { parseDirectConversationSelection, readCommunicationRule, type CommunicateInput, type CommunicateResult, type CommunicationRule, type DirectConversationSelection, type StartDirectInput, type StartDirectResult, } from '../backend-contracts/index.js';
59
59
  export { canonMediaDownloadUrl, sanitizeDownloadFileName, buildMediaContentDisposition, getStoredFileExtension } from '../backend-contracts/index.js';
60
- export * from './completed-output-journal.js';
@@ -0,0 +1,45 @@
1
+ import { type EndpointStore, type EndpointStoreTransaction } from './store.js';
2
+ /**
3
+ * A runtime's finished output, recorded before it is published: a turn's answer, or text a host sends with no turn. A
4
+ * host that dies between the answer and Canon's acceptance publishes exactly that output on restart and never runs the
5
+ * turn again. One record holds the answer, the inputs it answers and the audience it may reach, in the engine's own
6
+ * store, so recording it and settling its inputs once it is durable are each one transaction with the inbox.
7
+ */
8
+ export interface EndpointCompletion<T = unknown> {
9
+ version: 1;
10
+ id: string;
11
+ conversationId: string;
12
+ /** The inbox inputs it answers. They settle with it (`finishCompletion`); an uncertain input that no completion names is an orphan. */
13
+ inputIds: string[];
14
+ /** The audience its publication may reach (an encrypted room's group head, roster and admissions), or null. */
15
+ fence: unknown;
16
+ /** What the runtime answered, as its host records it. */
17
+ output: T;
18
+ createdAt: string;
19
+ }
20
+ /** Completions in the engine's store. Every write checks the current installation, as content writes do. */
21
+ export declare function createCompletions(input: {
22
+ store: EndpointStore;
23
+ ready(): Promise<unknown>;
24
+ assertOpen(): void;
25
+ assertCurrent(tx: EndpointStoreTransaction): Promise<void>;
26
+ /** Settles one inbox input inside the caller's transaction, as `setInboundState` does. */
27
+ settleInbound(tx: EndpointStoreTransaction, id: string, reason: string): Promise<void>;
28
+ }): {
29
+ save(record: EndpointCompletion): Promise<void>;
30
+ get<T>(id: string): Promise<EndpointCompletion<T> | undefined>;
31
+ /** Oldest first. */
32
+ list<T>(): Promise<Array<EndpointCompletion<T>>>;
33
+ /** One record for another, in one step: a Stop's cut keeps only what was already queued. */
34
+ replace(previousId: string, next: EndpointCompletion): Promise<void>;
35
+ /**
36
+ * The output is durable or withdrawn: its record goes, and in the same step `settle`'s inputs settle. An input already
37
+ * settled, or no longer in the inbox, is left as it is.
38
+ */
39
+ finish(id: string, settle?: {
40
+ inputIds: readonly string[];
41
+ reason: string;
42
+ }): Promise<void>;
43
+ /** The input ids every completion names, read inside the caller's transaction. */
44
+ answered(tx: EndpointStoreTransaction): Promise<Set<string>>;
45
+ };
@@ -10,6 +10,7 @@ import { type EndpointRecoveryMigration, type EndpointMigrationReceipt } from '.
10
10
  import { type EndpointBackgroundMediaAdapter, type EndpointMediaBytes, type EndpointMediaPipeline } from './media.js';
11
11
  import { type EndpointOrdering } from './queue.js';
12
12
  import { type EndpointInboundHandoff, type EndpointInboundStartResult } from './handoff.js';
13
+ import { type EndpointCompletion } from './completions.js';
13
14
  import { type ExternalInboundClaim } from './external-inbound.js';
14
15
  import type { EndpointExternalInboundClaimResult, EndpointExternalInboundOutcome } from '../backend-contracts/index.js';
15
16
  import { type MlsDevGroupClient } from './mls-dev-groups.js';
@@ -246,11 +247,25 @@ export interface Endpoint {
246
247
  }): Promise<boolean>;
247
248
  acceptInbound(options: EndpointInboundConsumerOptions): EndpointInboundConsumer;
248
249
  setInboundState(id: string, state: EndpointInboundState, reason?: string): Promise<void>;
250
+ /** `unanswered`: only inputs no completion names (`completions.ts`), read in the same transaction. */
249
251
  listInbound(options?: {
250
252
  states?: EndpointInboundState[];
251
253
  after?: string;
252
254
  limit?: number;
255
+ unanswered?: boolean;
253
256
  }): Promise<EndpointInboundRecord[]>;
257
+ /** A runtime's finished output, recorded before it is published (`completions.ts`). The same record again is a no-op. */
258
+ saveCompletion(record: EndpointCompletion): Promise<void>;
259
+ getCompletion<T = unknown>(id: string): Promise<EndpointCompletion<T> | undefined>;
260
+ /** Every recorded completion, oldest first. */
261
+ listCompletions<T = unknown>(): Promise<Array<EndpointCompletion<T>>>;
262
+ /** One completion for another, in one transaction. */
263
+ replaceCompletion(previousId: string, next: EndpointCompletion): Promise<void>;
264
+ /** The output is durable or withdrawn: the completion goes, and `settle`'s inputs settle, in one transaction. */
265
+ finishCompletion(id: string, settle?: {
266
+ inputIds: readonly string[];
267
+ reason: string;
268
+ }): Promise<void>;
254
269
  discardPending(): Promise<void>;
255
270
  logout(): Promise<void>;
256
271
  close(): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  export * from './engine.js';
2
+ export type { EndpointCompletion } from './completions.js';
2
3
  export * from './store.js';
3
4
  export * from './transport.js';
4
5
  export * from './observe-query.js';
@@ -1,8 +1,9 @@
1
1
  import type { Endpoint } from './engine.js';
2
2
  /** Transient writes are coalesced here; they never enter the durable outbox. */
3
3
  export declare function createEndpointTransientPublisher(endpoint: Pick<Endpoint, 'publish'>, clock?: () => number): {
4
- pulseTyping(conversationId: string): void;
5
- stopTyping: (conversationId: string) => void;
4
+ /** A keystroke in the composer wakes the room's agents: once per burst of typing (a 3 s pause ends one), and every 10 s
5
+ * of it. The hint carries no content; typing itself never leaves the device. */
6
+ prewarmAgents(conversationId: string): void;
6
7
  setPresence(state: "online" | "offline" | "hidden"): void;
7
8
  close(): void;
8
9
  };
package/dist/media.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  toAnthropicImageBlock,
19
19
  toAnthropicImageBlocksWithinBudget,
20
20
  uploadMediaFile
21
- } from "./chunks/chunk-ZPBOGOAD.js";
21
+ } from "./chunks/chunk-5YKGDUJX.js";
22
22
  import "./chunks/chunk-YJUPKU47.js";
23
23
  import "./chunks/chunk-YHDDOADK.js";
24
24
  import "./chunks/chunk-5WPAB5MB.js";
@@ -95,8 +95,8 @@ export declare function awaitErrand(directory: string, command: Pick<ErrandComma
95
95
  accepted: boolean;
96
96
  }>;
97
97
  export interface ErrandHandler {
98
- /** The command was taken already (its record or its journaled output exists): its file is only removed. */
99
- taken(id: string): boolean;
98
+ /** The command was taken already (its record or its saved output exists): its file is only removed. */
99
+ taken(id: string): boolean | Promise<boolean>;
100
100
  /**
101
101
  * The command, or why its file is refused. True once its effect (or refusal) is journaled, so the file may go; false
102
102
  * leaves it for the next scan. A throw is logged once per command and reason, and also leaves it.
@@ -68,16 +68,49 @@ export interface RunnerOptions {
68
68
  sessionApprovalForDestructive?: boolean;
69
69
  /** How long running turns may finish on shutdown before they are aborted (default 10 s). */
70
70
  shutdownGraceMs?: number;
71
+ /**
72
+ * Idle (agent-wake.md): `onIdle` is called once the runner has been idle (`activity().idle`) for this long, in ms, and
73
+ * again only after new activity and another idle period. The runner does nothing else about it: the host decides, as the
74
+ * CLI's --idle-timeout stops and exits, or a hosting platform suspends the agent's machine until Canon's wake callback.
75
+ */
76
+ idleTimeoutMs?: number;
77
+ /** Idle: called with the activity that showed it; it never names a room, message or content. */
78
+ onIdle?(activity: RunnerActivity): void;
71
79
  /** Never receives prompt or reply text. */
72
80
  log?: (line: string) => void;
73
81
  }
82
+ /**
83
+ * What the runner has in hand, for a host that stops or suspends an idle agent (agent-wake.md). Counts only. Idle is all
84
+ * of them 0: once stopped or suspended, the agent loses nothing (input waits in Canon and in its inbox) but answers only
85
+ * when Canon's wake callback, or its host, starts it again.
86
+ */
87
+ export interface RunnerActivity {
88
+ idle: boolean;
89
+ /** When the runner last had something to do (epoch ms), or when it started. */
90
+ lastActiveAt: number;
91
+ /** Turns running or starting, errand turns, restart recovery and New session resets included. */
92
+ turns: number;
93
+ /** Input waiting for a turn: in the inbox, being admitted, or queued behind a running turn (whose own input is in `turns`). */
94
+ inputs: number;
95
+ /** Errands waiting for their turn. */
96
+ errands: number;
97
+ /** In-flight proactive/relay calls plus operations Canon has not answered, including replies, cards and group changes. */
98
+ outbound: number;
99
+ }
74
100
  export interface Runner {
75
101
  /** Connects and starts taking input; resolves once listening is set up. */
76
102
  start(): Promise<void>;
77
103
  /** Stops intake, releases unsubmitted input, lets running turns finish within the shutdown grace. */
78
104
  stop(): Promise<void>;
79
- /** A proactive send outside any turn (cron): the `communicate` tool's handler, under the runner's identity. */
105
+ /**
106
+ * A proactive send outside any turn (cron), under the runner's identity, with the `communicate` tool's input. Text to a
107
+ * conversation is encrypted for the members it has now, journaled first and sent once, even across a restart; a new
108
+ * conversation is founded encrypted by this device. `messageId` makes a retry the same message or start. Other actions
109
+ * go to Canon as they are.
110
+ */
80
111
  communicate(input: CommunicateInput): Promise<CommunicateResult>;
112
+ /** What the runner has in hand now, for a host's idle policy (agent-wake.md). Only between start and stop. */
113
+ activity(): Promise<RunnerActivity>;
81
114
  /**
82
115
  * Relay mode (§11.6): an encrypted room's history as this device verified it and Canon accepted it, newest first, cut
83
116
  * to the channel's response budget. `deadline` (epoch ms, default the channel's answer budget) bounds the read.
@@ -0,0 +1,16 @@
1
+ import type { AgentWakeConfiguration } from '../internal/backend-contracts/index.js';
2
+ /**
3
+ * A wake callback's signing secret: `whsec_` and the standard base64 (padded) of 32 to 64 random bytes. Canon takes at
4
+ * least 32 for a wake callback, and a receiver such as Agent Platform's `/wake/:bindingId` at most 64, padded.
5
+ */
6
+ export declare function isWakeSecret(value: unknown): value is string;
7
+ /** The same checks Canon makes before it sends the callback its verification challenge; Canon checks again. */
8
+ export declare function parseWakeConfiguration(value: unknown): AgentWakeConfiguration;
9
+ /**
10
+ * `--wake-config <path>` (agent-wake.md): the agent's signed wake callback. A private JSON file (0600, owned by this user,
11
+ * one name, no links; on Windows only this user, SYSTEM and Administrators may reach it and its folder), since it holds
12
+ * the callback's signing secret: `{ "version": 1, "callbackUrl", "secret", "conversationIds" }`. Its text is never logged.
13
+ */
14
+ export declare function loadWakeConfiguration(path: string): Promise<AgentWakeConfiguration>;
15
+ /** `--idle-timeout`: whole seconds, or a number with `s`, `m` or `h`; at least one second. Returns milliseconds. */
16
+ export declare function parseIdleTimeout(value: string): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "13.0.3",
3
+ "version": "13.2.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",