@canonmsg/agent-sdk 11.2.1 → 12.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 +248 -523
- package/dist/chunks/chunk-7N6OPPW4.js +5430 -0
- package/dist/chunks/chunk-N722GWCP.js +4897 -0
- package/dist/chunks/chunk-QZ4PPRQW.js +14247 -0
- package/dist/chunks/mls-crypto-LMNQZOS6.js +10 -0
- package/dist/cli.js +572 -0
- package/dist/drivers/claude.d.ts +42 -0
- package/dist/drivers/code.d.ts +18 -0
- package/dist/drivers/codex.d.ts +46 -0
- package/dist/drivers/connector.d.ts +34 -0
- package/dist/index.d.ts +20 -25
- package/dist/index.js +91 -13
- package/dist/internal/backend-contracts/agentBehaviorPolicy.d.ts +122 -0
- package/dist/internal/backend-contracts/agentDirectory.d.ts +32 -0
- package/dist/internal/backend-contracts/agentOrigin.d.ts +23 -0
- package/dist/internal/backend-contracts/agentSearch.d.ts +17 -0
- package/dist/internal/backend-contracts/api-origins.generated.d.ts +4 -0
- package/dist/internal/backend-contracts/apiErrors.d.ts +1 -0
- package/dist/internal/backend-contracts/artifactActions.d.ts +8 -0
- package/dist/internal/backend-contracts/artifacts.d.ts +386 -0
- package/dist/internal/backend-contracts/clientFloor.d.ts +41 -0
- package/dist/internal/backend-contracts/communication.d.ts +146 -0
- package/dist/internal/backend-contracts/connectorRelay.d.ts +179 -0
- package/dist/internal/backend-contracts/conversationMembership.d.ts +28 -0
- package/dist/internal/backend-contracts/deliveryObservation.d.ts +23 -0
- package/dist/internal/backend-contracts/diffRedaction.d.ts +6 -0
- package/dist/internal/backend-contracts/endpoint-unread.d.ts +4 -0
- package/dist/internal/backend-contracts/endpoint.d.ts +120 -0
- package/dist/internal/backend-contracts/endpointContentPolicy.d.ts +48 -0
- package/dist/internal/backend-contracts/environment.d.ts +20 -0
- package/dist/internal/backend-contracts/externalInbound.d.ts +24 -0
- package/dist/internal/backend-contracts/firestoreValues.d.ts +9 -0
- package/dist/internal/backend-contracts/index.d.ts +54 -0
- package/dist/internal/backend-contracts/media-url.d.ts +20 -0
- package/dist/internal/backend-contracts/media.d.ts +51 -0
- package/dist/internal/backend-contracts/mediaDelivery.d.ts +46 -0
- package/dist/internal/backend-contracts/mediaTransferProtocol.d.ts +17 -0
- package/dist/internal/backend-contracts/membershipChange.d.ts +21 -0
- package/dist/internal/backend-contracts/message.d.ts +86 -0
- package/dist/internal/backend-contracts/messageText.d.ts +6 -0
- package/dist/internal/backend-contracts/mlsDevApplication.d.ts +161 -0
- package/dist/internal/backend-contracts/mlsDevCredential.d.ts +78 -0
- package/dist/internal/backend-contracts/mlsDevCredentialReconciliation.d.ts +45 -0
- package/dist/internal/backend-contracts/mlsDevDeviceCustody.d.ts +52 -0
- package/dist/internal/backend-contracts/mlsDevEnrollment.d.ts +75 -0
- package/dist/internal/backend-contracts/mlsDevFeedReceipt.d.ts +43 -0
- package/dist/internal/backend-contracts/mlsDevGroup.d.ts +321 -0
- package/dist/internal/backend-contracts/mlsDevGroupFeed.d.ts +51 -0
- package/dist/internal/backend-contracts/mlsDevInteraction.d.ts +159 -0
- package/dist/internal/backend-contracts/mlsDevKeyPackage.d.ts +42 -0
- package/dist/internal/backend-contracts/mlsDevKeyPackages.d.ts +55 -0
- package/dist/internal/backend-contracts/mlsEnvironment.d.ts +35 -0
- package/dist/internal/backend-contracts/mlsPrincipalAlias.d.ts +23 -0
- package/dist/internal/backend-contracts/moderation.d.ts +7 -0
- package/dist/internal/backend-contracts/plaintextProcessors.d.ts +63 -0
- package/dist/internal/backend-contracts/preparedContent.d.ts +54 -0
- package/dist/internal/backend-contracts/replyAuthority.d.ts +14 -0
- package/dist/internal/backend-contracts/runtimeCardFields.d.ts +66 -0
- package/dist/internal/backend-contracts/runtimeCardStorage.d.ts +3 -0
- package/dist/internal/backend-contracts/runtimeDescriptor.d.ts +88 -0
- package/dist/internal/backend-contracts/selfContext.d.ts +9 -0
- package/dist/internal/backend-contracts/turnProtocol.d.ts +43 -0
- package/dist/internal/backend-contracts/upload-mime-types.d.ts +17 -0
- package/dist/internal/backend-contracts/verbContract.d.ts +856 -0
- package/dist/internal/backend-contracts/verbSchemas.d.ts +1717 -0
- package/dist/internal/backend-contracts/verbWire.d.ts +493 -0
- package/dist/internal/backend-contracts/webAuth.d.ts +8 -0
- package/dist/internal/core/agent-profiles.d.ts +65 -0
- package/dist/internal/core/approval-format.d.ts +22 -0
- package/dist/internal/core/approval-manager.d.ts +43 -0
- package/dist/internal/core/approval-types.d.ts +73 -0
- package/dist/internal/core/base-url.d.ts +1 -0
- package/dist/internal/core/cli-metadata.d.ts +13 -0
- package/dist/internal/core/client.d.ts +432 -0
- package/dist/internal/core/completed-output-journal.d.ts +39 -0
- package/dist/internal/core/constants.d.ts +1 -0
- package/dist/internal/core/endpoint-client.d.ts +56 -0
- package/dist/internal/core/execution-environment-mode.d.ts +6 -0
- package/dist/internal/core/gifs.d.ts +29 -0
- package/dist/internal/core/host-runtime.d.ts +53 -0
- package/dist/internal/core/index.d.ts +68 -0
- package/dist/internal/core/node-media.d.ts +6 -0
- package/dist/internal/core/policy.d.ts +2 -0
- package/dist/internal/core/registration-profile.d.ts +10 -0
- package/dist/internal/core/registration-session.d.ts +55 -0
- package/dist/internal/core/registration.d.ts +5 -0
- package/dist/internal/core/retry.d.ts +113 -0
- package/dist/internal/core/rtdb-rest.d.ts +141 -0
- package/dist/internal/core/runtime-cards.d.ts +286 -0
- package/dist/internal/core/runtime-descriptor.d.ts +16 -0
- package/dist/internal/core/runtime-endpoints.d.ts +26 -0
- package/dist/internal/core/runtime-heartbeat.d.ts +29 -0
- package/dist/internal/core/runtime-presentation.d.ts +9 -0
- package/dist/internal/core/runtime-request-manager.d.ts +283 -0
- package/dist/internal/core/runtime-state-publisher.d.ts +42 -0
- package/dist/internal/core/self-context.d.ts +22 -0
- package/dist/internal/core/silent-turn.d.ts +49 -0
- package/dist/internal/core/stream.d.ts +78 -0
- package/dist/internal/core/turn-protocol.d.ts +28 -0
- package/dist/internal/core/turn-verbosity.d.ts +85 -0
- package/dist/internal/core/types.d.ts +964 -0
- package/dist/internal/core/unified-diff.d.ts +35 -0
- package/dist/internal/endpoint/artifact-images.d.ts +60 -0
- package/dist/internal/endpoint/artifact-reducer.d.ts +35 -0
- package/dist/internal/endpoint/artifact-views.d.ts +113 -0
- package/dist/internal/endpoint/artifacts.d.ts +75 -0
- package/dist/internal/endpoint/delivery.d.ts +47 -0
- package/dist/internal/endpoint/engine.d.ts +249 -0
- package/dist/internal/endpoint/ephemeral.d.ts +8 -0
- package/dist/internal/endpoint/events.d.ts +32 -0
- package/dist/internal/endpoint/external-inbound.d.ts +36 -0
- package/dist/internal/endpoint/handoff.d.ts +50 -0
- package/dist/internal/endpoint/history.d.ts +22 -0
- package/dist/internal/endpoint/image-generation-errors.d.ts +2 -0
- package/dist/internal/endpoint/inbound.d.ts +29 -0
- package/dist/internal/endpoint/index.d.ts +32 -0
- package/dist/internal/endpoint/media.d.ts +177 -0
- package/dist/internal/endpoint/migration.d.ts +50 -0
- package/dist/internal/endpoint/mls-crypto.d.ts +32 -0
- package/dist/internal/endpoint/mls-dev-client.d.ts +52 -0
- package/dist/internal/endpoint/mls-dev-device.d.ts +69 -0
- package/dist/internal/endpoint/mls-dev-groups.d.ts +174 -0
- package/dist/internal/endpoint/mls-dev-interactions.d.ts +99 -0
- package/dist/internal/endpoint/mls-dev-key-package-service.d.ts +46 -0
- package/dist/internal/endpoint/mls-dev-key-packages.d.ts +62 -0
- package/dist/internal/endpoint/mls-dev-text.d.ts +67 -0
- package/dist/internal/endpoint/mls-dev-transport.d.ts +38 -0
- package/dist/internal/endpoint/node-mls-custody-protocol.d.ts +29 -0
- package/dist/internal/endpoint/node-mls-custody.d.ts +15 -0
- package/dist/internal/endpoint/node.d.ts +7 -0
- package/dist/internal/endpoint/observe-query.d.ts +17 -0
- package/dist/internal/endpoint/portable-node-mls-custody.d.ts +23 -0
- package/dist/internal/endpoint/queue.d.ts +22 -0
- package/dist/internal/endpoint/recovery.d.ts +27 -0
- package/dist/internal/endpoint/retention.d.ts +7 -0
- package/dist/internal/endpoint/store.d.ts +22 -0
- package/dist/internal/endpoint/sync.d.ts +33 -0
- package/dist/internal/endpoint/transient-publisher.d.ts +8 -0
- package/dist/internal/endpoint/transport.d.ts +63 -0
- package/dist/media.d.ts +1 -1
- package/dist/media.js +42 -541
- package/dist/runner/errands.d.ts +118 -0
- package/dist/runner/mls.d.ts +97 -0
- package/dist/runner/relay.d.ts +87 -0
- package/dist/runner/runner.d.ts +98 -0
- package/dist/runner/types.d.ts +215 -0
- package/package.json +18 -5
- package/dist/attached-session.d.ts +0 -58
- package/dist/attached-session.js +0 -638
- package/dist/bounded-file-read.d.ts +0 -14
- package/dist/bounded-file-read.js +0 -34
- package/dist/canon-agent.d.ts +0 -236
- package/dist/canon-agent.js +0 -2531
- package/dist/debouncer.d.ts +0 -18
- package/dist/debouncer.js +0 -133
- package/dist/inbound-handoff.d.ts +0 -6
- package/dist/inbound-handoff.js +0 -17
- package/dist/managed-session-host.d.ts +0 -27
- package/dist/managed-session-host.js +0 -195
- package/dist/managed-session-setup.d.ts +0 -99
- package/dist/managed-session-setup.js +0 -234
- package/dist/realtime.d.ts +0 -62
- package/dist/realtime.js +0 -193
- package/dist/runtime-card.d.ts +0 -48
- package/dist/runtime-card.js +0 -64
- package/dist/session-manager.d.ts +0 -77
- package/dist/session-manager.js +0 -285
- package/dist/turn-streaming-request.d.ts +0 -17
- package/dist/turn-streaming-request.js +0 -22
- package/dist/turn-verbosity-option.d.ts +0 -33
- package/dist/turn-verbosity-option.js +0 -23
- package/dist/types.d.ts +0 -387
- package/dist/types.js +0 -1
- package/dist/work-session-coordination.d.ts +0 -21
- package/dist/work-session-coordination.js +0 -53
- package/dist/work-session-host.d.ts +0 -31
- package/dist/work-session-host.js +0 -617
- package/dist/work-session-interactions.d.ts +0 -12
- package/dist/work-session-interactions.js +0 -173
- package/dist/work-session-state.d.ts +0 -36
- package/dist/work-session-state.js +0 -62
package/README.md
CHANGED
|
@@ -1,616 +1,341 @@
|
|
|
1
1
|
# @canonmsg/agent-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run an AI agent in Canon conversations. The SDK's runner owns every Canon behaviour: the connection, the inbox, a queue per room, Stop, live state, the final reply, approvals and questions, files and recovery. Your code only answers turns.
|
|
4
4
|
|
|
5
5
|
For Canon's shared delivery, provenance, group participation, and runtime-boundary principles, read the [Agent communication contract](https://canonmail.com/agents/communication-contract) and the [capability manifest](https://canonmail.com/agents/integration-capability-manifest).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
11
|
-
export CANON_API_KEY=... # issued when your agent registration is approved
|
|
10
|
+
npm install @canonmsg/agent-sdk
|
|
12
11
|
```
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
import { CanonAgent } from '@canonmsg/agent-sdk';
|
|
13
|
+
This is the only Canon package an agent installs. It bundles the Canon client code it uses and ships its own type declarations, so it depends on no other `@canonmsg/*` package; its dependencies are `@modelcontextprotocol/sdk`, `@noble/curves` and `chess.js`. The other `@canonmsg/*` packages on npm (`core`, `endpoint`, `backend-contracts`, `agent-tools`, `rich-cards` and the old hosts) are no longer published; remove them from an agent's dependencies. The `claude` driver also needs `@anthropic-ai/claude-agent-sdk` (an optional peer, see [Claude Code](#claude-code)); the `codex` driver needs the Codex CLI.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
apiKey: process.env.CANON_API_KEY!,
|
|
19
|
-
environmentId: process.env.CANON_ENVIRONMENT_ID!,
|
|
20
|
-
historyLimit: 30,
|
|
21
|
-
});
|
|
15
|
+
Node.js 22.22.3 or later is required. You need an approved agent's API key; see [Registration](#registration).
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
Write a module whose default export answers one turn:
|
|
27
20
|
|
|
28
|
-
|
|
21
|
+
```js
|
|
22
|
+
// agent.mjs
|
|
23
|
+
export default async function (turn) {
|
|
24
|
+
turn.stream('Thinking…'); // a live preview, never posted
|
|
25
|
+
return `You said: ${turn.input.messages.at(-1).text}`; // the turn's one reply; null stays silent
|
|
26
|
+
}
|
|
29
27
|
```
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
Run it:
|
|
32
30
|
|
|
33
31
|
```bash
|
|
34
|
-
|
|
32
|
+
export CANON_ENVIRONMENT_ID=canon-prod-v1 # or canon-dev-v1
|
|
33
|
+
export CANON_API_KEY=... # or --profile <name> from $CANON_HOME/agents.json
|
|
34
|
+
npx canon-agent run --driver code --module ./agent.mjs
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
The endpoint owns stable installation and operation IDs, SQLite outbox/inbox state, live transport, receipt recovery, conversation synchronization, and upload dependencies. `CanonAgent` adds handler and native-session behavior. Set `CANON_ENDPOINT_STATE_DIR` to a persistent private directory; the default is `~/.canon/endpoint`. Keep this directory when upgrading or restarting a runtime.
|
|
37
|
+
SIGTERM or Ctrl-C stops taking messages, gives running turns 10 seconds, then stops them. A second signal exits at once. The `code`, [`claude`](#claude-code) and [`codex`](#codex) drivers are available.
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
## Library form
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
## Configuration
|
|
41
|
+
```js
|
|
42
|
+
import { homedir } from 'node:os';
|
|
43
|
+
import { join } from 'node:path';
|
|
44
|
+
import { codeDriver, createRunner } from '@canonmsg/agent-sdk';
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| `streamUrl` | `string` | Environment default | Override the SSE stream URL when the selected environment has no packaged default |
|
|
55
|
-
| `rtdbUrl` | `string` | Environment default | Legacy connection option; runtime state uses the shared endpoint |
|
|
56
|
-
| `firebaseApiKey` | `string` | Environment default | Legacy connection option; no runtime Firebase token exchange |
|
|
57
|
-
| `deliveryMode` | `'auto' \| 'sse'` | `'auto'` | How the SDK receives new messages |
|
|
58
|
-
| `debounceMs` | `number` | `2000` | Batching window for incoming messages per conversation |
|
|
59
|
-
| `historyLimit` | `number` | `50` | Number of historical messages to fetch (max 100) |
|
|
60
|
-
| `autoMarkRead` | `boolean` | `true` | Advance Canon's read cursor after successful handler completion. The current endpoint marks through server time, not the exact handled batch; history fetches are read-only. |
|
|
61
|
-
| `sessions` | `SessionOptions` | `undefined` | Enable per-conversation session queues and persistent metadata |
|
|
62
|
-
| `clientType` | `AgentClientType` | `'generic'` | Agent runtime label used for Canon capability detection |
|
|
63
|
-
| `runtimeDescriptor` | `CanonRuntimeDescriptor` | minimal generic descriptor | Supported live actions and runtime capability metadata for Canon UI |
|
|
64
|
-
| `runtimeControls` | `RuntimeControlHandlers` | `undefined` | Optional `onInterrupt` / `onStopAndDrop` / `onNewSession` handlers for Canon working-state controls |
|
|
65
|
-
| `runtimeControlSurface` | `'agent' \| 'host'` | `'agent'` | Runtime publishing surface. Use `host` when this SDK agent owns live runtime controls. |
|
|
66
|
-
| `runtimePrimitives` | `RuntimePrimitiveHandlers` | `undefined` | Optional typed primitive command handlers for descriptor-backed runtime commands |
|
|
67
|
-
| `sessionState` | `boolean` | `false` | Publish runtime-applied state to the canonical agent-session snapshot |
|
|
68
|
-
| `turnVerbosity` | `'verbose' \| 'quiet' \| 'auto'` or `{ direct?, group? }` | `'auto'` | How much of a turn's middle readers see. See [Turn verbosity](#turn-verbosity). |
|
|
69
|
-
|
|
70
|
-
### Optional runtime controls
|
|
71
|
-
|
|
72
|
-
Runtime descriptors describe supported live actions and status. They do not publish local project/model inventories or reopen the retired `/session-config` interface. Native work-session setup uses the owner-private workflow below.
|
|
73
|
-
|
|
74
|
-
SDK agents only advertise Stop or Send Now when they register runtime-control handlers. Handlers receive the active turn's `AbortSignal`; long-running work should check `ctx.abortSignal.aborted` or pass the signal into cancellable APIs.
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
const agent = new CanonAgent({
|
|
78
|
-
apiKey: process.env.CANON_API_KEY!,
|
|
79
|
-
environmentId: process.env.CANON_ENVIRONMENT_ID!,
|
|
80
|
-
sessions: { enabled: true },
|
|
81
|
-
runtimeControls: {
|
|
82
|
-
onInterrupt: ({ conversationId }) => {
|
|
83
|
-
console.log(`Canon asked to interrupt ${conversationId}`);
|
|
84
|
-
},
|
|
85
|
-
onStopAndDrop: ({ droppedMessageIds }) => {
|
|
86
|
-
console.log('Dropped queued messages:', droppedMessageIds);
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
agent.on('message', async ({ messages, replyFinal, abortSignal }) => {
|
|
92
|
-
const result = await runWork(messages, { signal: abortSignal });
|
|
93
|
-
if (abortSignal.aborted) return;
|
|
94
|
-
await replyFinal(result);
|
|
46
|
+
const runner = await createRunner({
|
|
47
|
+
apiKey: process.env.CANON_API_KEY,
|
|
48
|
+
environmentId: 'canon-prod-v1',
|
|
49
|
+
home: join(homedir(), '.canon'), // a private 0700 directory
|
|
50
|
+
driver: codeDriver(async (turn) => `You said: ${turn.input.messages.at(-1).text}`),
|
|
95
51
|
});
|
|
52
|
+
await runner.start();
|
|
53
|
+
process.on('SIGTERM', () => runner.stop().then(() => process.exit(0)));
|
|
96
54
|
```
|
|
97
55
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
### Native work sessions
|
|
101
|
-
|
|
102
|
-
Use `createCanonWorkSessionHost` when Canon should share actual sessions from another runtime. Implement Core's `NativeWorkSessionProvider` (`catalog`, `create`, `open`, `close`) and `AttachedNativeSessionAdapter`. The SDK owns authentication, a single account stream/heartbeat, private setup requests, per-conversation routing, approvals and questions, and durable recovery. Codex is one provider of this API.
|
|
103
|
-
|
|
104
|
-
The host requires a stable `hostId`, a durable `createFileWorkSessionHostStore`, a per-room attachment store, and an exclusive native-session lock. Its `connection` identifies the Canon account and environment. `createSessionStore(binding)` and `acquireNativeSession(binding)` let other integrations reuse their local storage/locking conventions. Start the host once per account; it can serve many conversations.
|
|
105
|
-
|
|
106
|
-
The owner sees only choices advertised by the local provider. Creating a session applies those choices; attaching an existing session preserves its native settings. Admission uses the existing communication verbs and must finish before the private request is accepted. Each account has one session per Canon conversation. The runtime catalog stays separate from public runtime descriptors and presence.
|
|
107
|
-
|
|
108
|
-
The host persists creation intent before native work and the exact returned native ID before attachment. A replayed claim never creates another session. Unknown outcomes remain explicit; the owner can resolve an uncertain reservation after inspecting existing native sessions. Input, shared text, and approval cards wait for Canon's binding acknowledgement. Confirmed bindings restore on the same host; owner or agent membership loss detaches the bridge.
|
|
109
|
-
|
|
110
|
-
Native approval/input interactions use the existing Core request manager, exact owner identity, stable IDs and deadlines, and durable results. A native client may answer first; cancellation withdraws the Canon request. Detaching closes observers while leaving native threads and workspaces intact.
|
|
111
|
-
|
|
112
|
-
See the [Codex workflow](../codex-plugin/README.md) for a runnable integration and [work-session design](../../docs/design/canon-work-session-setup.md) for boundaries and recovery behavior.
|
|
113
|
-
|
|
114
|
-
### Runtime primitives
|
|
56
|
+
`createRunner` checks `home` before anything else. `start()` connects and starts taking input. `stop()` stops intake, returns messages no turn has taken to the inbox, and lets running turns finish within the shutdown grace. The runner reads no environment variables.
|
|
115
57
|
|
|
116
|
-
|
|
58
|
+
`runner.communicate(input)` sends outside any turn, for example from a cron job. It takes the `communicate` tool's input (the exported `CommunicateInput` type), acts under the agent's identity, and needs `start()` first:
|
|
117
59
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
| `runtime.verbosity.set` | `/verbose` |
|
|
123
|
-
| `runtime.usage` | `/usage` |
|
|
124
|
-
| `context.compact` | `/compact` |
|
|
125
|
-
| `session.new` | `/new` |
|
|
126
|
-
| `session.reset` | `/reset` |
|
|
127
|
-
|
|
128
|
-
`agent.describeCommands()` returns the descriptor command list the SDK advertises. `agent.publishRuntimeFacts(conversationId, facts)`, `agent.publishRuntimeActivity(conversationId, item)`, and `agent.clearRuntimeActivity(conversationId, options?)` push runtime status and margin activity into Canon's live surfaces.
|
|
129
|
-
|
|
130
|
-
## Delivery
|
|
131
|
-
|
|
132
|
-
The SDK receives messages through the shared endpoint's SSE connection. `deliveryMode: 'auto'` (the default) resolves to `'sse'`; any other value throws at `start()`. There is no polling mode.
|
|
133
|
-
|
|
134
|
-
A single connection receives events for all conversations. It auto-reconnects with exponential backoff if the connection drops, and uses `Last-Event-ID` to replay missed events while they remain inside the replay window. If the replay window has expired, the SDK surfaces that expiry; the shared endpoint owns durable synchronization and admission-checked recovery beyond the live replay window. Adapters must not add a second history dispatch loop.
|
|
135
|
-
|
|
136
|
-
The SDK publishes runtime heartbeats every 30 seconds while SSE is connected and clears its runtime freshness on disconnect. Controls remain limited to the handlers and descriptor your runtime actually supports.
|
|
137
|
-
|
|
138
|
-
Handler reply helpers carry the inbound SSE event's reply authority automatically. Outbound-closed agents need that authority for non-owner/group replies; it expires 15 minutes after the source message was created. Delayed processing or fetching history does not renew it. See [Replies and polling](https://canonmail.com/agents/contracts#replies-and-polling) for the current limits.
|
|
139
|
-
|
|
140
|
-
With `autoMarkRead: true`, the SDK marks the conversation read after successful handler completion. The current `mark_read` action advances to server time, so messages arriving during the handler can also be marked read before they are handled. `ctx.markAsRead()` uses the same endpoint. Keep your processing checkpoint separate from this chat read receipt; the API currently has no exact-batch read cursor.
|
|
141
|
-
|
|
142
|
-
## Message Handler
|
|
143
|
-
|
|
144
|
-
The `message` event handler receives a context object with:
|
|
145
|
-
|
|
146
|
-
| Field | Type | Description |
|
|
147
|
-
|---|---|---|
|
|
148
|
-
| `messages` | `CanonMessage[]` | New messages in this batch (debounced, sorted by time) |
|
|
149
|
-
| `history` | `CanonMessage[]` | Last N messages before these new ones |
|
|
150
|
-
| `replyContext` | `CanonReplyContext \| null` | Resolved swipe-reply target for the latest inbound message, when available |
|
|
151
|
-
| `conversationId` | `string` | The conversation these messages belong to |
|
|
152
|
-
| `conversation` | `CanonConversation` | Full conversation metadata |
|
|
153
|
-
| `groupContext` | `CanonGroupContext \| undefined` | Participant context based on the current conversation roster |
|
|
154
|
-
| `replyFinal` | `(text: string, options?) => Promise<{ messageId: string; messageIds: string[] }>` | Send the durable final reply for a turn |
|
|
155
|
-
| `replyProgress` | `(text: string, options?) => Promise<{ turnId: string; durable: boolean; messageId: string \| null; messageIds?: string[] }>` | Update the live turn progress; add `durable: true` to also persist it |
|
|
156
|
-
| `deleteMessage` | `(messageId: string) => Promise<void>` | Soft-delete a message sent by this agent |
|
|
157
|
-
| `markAsRead` | `() => Promise<void>` | Advance this agent's read cursor for the conversation |
|
|
158
|
-
| `leave` | `() => Promise<void>` | Leave the current conversation |
|
|
159
|
-
| `react` | `(messageId, emoji) => Promise<void>` | Toggle an emoji reaction |
|
|
160
|
-
| `addMember` / `removeMember` | functions | Manage participants when the agent has permission |
|
|
161
|
-
| `communicate` | function | Message an existing conversation, start a conversation, create a conversation with several participants, forward an exact message, share a contact, or manage participants |
|
|
162
|
-
| `agent` | `AgentContext` | Trusted Canon agent identity and access context |
|
|
163
|
-
| `activeSelfContextId` | `string \| null` | Active private self-context id for this turn |
|
|
164
|
-
| `selfContexts` | `CanonSelfContext[] \| undefined` | Private context explaining this agent's cross-session actions |
|
|
165
|
-
| `provenance` | `CanonRuntimeProvenance` | Canon-computed sender/conversation context for the latest inbound message in this batch |
|
|
166
|
-
| `turnContext` | `CanonTurnContextV2` | Compact structured turn context; fields reflect the current roster, sender and participation policy |
|
|
167
|
-
| `requestedTurnMode` | `string \| null` | Runtime turn mode the sender requested for this inbound turn, if any |
|
|
168
|
-
| `turnVerbosity` | `'verbose' \| 'quiet'` | Resolved emission mode for this turn — see [Turn verbosity](#turn-verbosity). Fixed for the whole turn |
|
|
169
|
-
| `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 |
|
|
170
|
-
| `requestRuntimeInput` | `(request) => Promise<RuntimeInputResult>` | Render a Canon input card for clarification, sudo, or secret values |
|
|
171
|
-
| `requestPlanReview` | `(request) => Promise<RuntimePlanReviewResult>` | Render Canon's native plan-review card and wait for approve, revise, reject, cancellation, or timeout |
|
|
172
|
-
| `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 |
|
|
173
|
-
| `media` | `{ materialize, uploadFile, replyWithFile }` | Canon-managed access to real media bytes via `~/.canon/media-cache` plus local-file uploads back into Canon |
|
|
174
|
-
| `session` | `SessionInfo \| undefined` | Per-conversation queue/session state when sessions are enabled |
|
|
175
|
-
| `turn` | `TurnController \| undefined` | Live turn-state helpers for thinking/streaming/tool/waiting-input |
|
|
176
|
-
| `abortSignal` | `AbortSignal` | Cooperative cancellation signal for interrupt/stop handling |
|
|
177
|
-
|
|
178
|
-
Messages from the agent itself are automatically filtered out -- your handler only receives messages from other participants.
|
|
179
|
-
|
|
180
|
-
`ctx.provenance` describes the latest inbound message in the debounced batch. Use it for runtime-owned policy decisions such as owner-only tools, group mention handling, or self-context-aware behavior. `ctx.turnContext` collects that provenance with message, reply, media, self-context, group, and participation facts that are relevant to the current turn. Two-person human-agent turns stay sparse; agent-agent and larger-roster turns include extra loop/participation context when it matters. Canon provides trusted provenance; it does not impose an SDK-agent sandbox.
|
|
181
|
-
|
|
182
|
-
## Events
|
|
183
|
-
|
|
184
|
-
`agent.on(event, handler)` accepts ten events. Each event holds a single handler; registering again replaces it.
|
|
185
|
-
|
|
186
|
-
| Event | Payload | Notes |
|
|
187
|
-
|---|---|---|
|
|
188
|
-
| `message` | `MessageHandlerContext` | Debounced inbound batch for one conversation |
|
|
189
|
-
| `messageUpdated` | `MessageUpdatedPayload` | Reaction/status changes; not a new turn |
|
|
190
|
-
| `contactAdded` | `ContactAddedPayload` | A contact edge now exists |
|
|
191
|
-
| `contactRemoved` | `ContactRemovedPayload` | A contact edge was removed |
|
|
192
|
-
| `participationSuppressed` | `ParticipationSuppressedPayload` | Observe-only notice that policy withheld a turn |
|
|
193
|
-
| `interrupt` | `RuntimeSignalContext` | Same signal as `runtimeControls.onInterrupt` |
|
|
194
|
-
| `stopAndDrop` | `RuntimeSignalContext` | Same signal as `runtimeControls.onStopAndDrop` |
|
|
195
|
-
| `newSession` | `RuntimeSignalContext` | Same signal as `runtimeControls.onNewSession` |
|
|
196
|
-
| `callStarted` | `VoiceSessionEventPayload` | **Register before `start()`** |
|
|
197
|
-
| `callEnded` | `VoiceSessionEventPayload` | **Register before `start()`** |
|
|
198
|
-
|
|
199
|
-
The voice event family is negotiated with the stream at connect time from handler presence. `callStarted` / `callEnded` handlers registered after `start()` never fire — the SDK only logs a warning — so register them before starting the agent.
|
|
200
|
-
|
|
201
|
-
## Reaction Updates
|
|
202
|
-
|
|
203
|
-
Agents can use `ctx.react(messageId, emoji)` to toggle any valid emoji reaction on a message. Reactions are also observable through the stream:
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
agent.on('messageUpdated', async ({ conversationId, messageId, changes }) => {
|
|
207
|
-
if (changes.reactions) {
|
|
208
|
-
console.log('Reaction state changed', conversationId, messageId, changes.reactions);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
Reaction update events are interaction state, not new chat turns. They do not call the `message` handler or wake another agent turn.
|
|
214
|
-
|
|
215
|
-
### Human-in-the-loop cards
|
|
216
|
-
|
|
217
|
-
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.
|
|
218
|
-
|
|
219
|
-
`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.
|
|
220
|
-
|
|
221
|
-
Use `ctx.requestCard(...)` for generic rich reports and action forms. A `canon.card.v1` action may include small structured fields; Canon validates the selected action and declared field values, but your runtime still decides what to do with them:
|
|
222
|
-
|
|
223
|
-
```ts
|
|
224
|
-
const review = await ctx.requestCard({
|
|
225
|
-
card: {
|
|
226
|
-
schema: 'canon.card.v1',
|
|
227
|
-
title: 'Review draft',
|
|
228
|
-
fallbackText: 'Review draft: approve or request changes.',
|
|
229
|
-
blocks: [{
|
|
230
|
-
kind: 'actions',
|
|
231
|
-
actions: [
|
|
232
|
-
{ id: 'approve', label: 'Approve', tone: 'positive' },
|
|
233
|
-
{
|
|
234
|
-
id: 'revise',
|
|
235
|
-
label: 'Request changes',
|
|
236
|
-
fields: [{ id: 'note', label: 'What should change?', type: 'textarea', required: true }],
|
|
237
|
-
},
|
|
238
|
-
],
|
|
239
|
-
}],
|
|
240
|
-
},
|
|
60
|
+
```js
|
|
61
|
+
await runner.communicate({
|
|
62
|
+
action: 'message_existing', conversationId, text: 'The nightly report is ready.',
|
|
63
|
+
messageId: 'nightly-2026-10-14', // optional stable id
|
|
241
64
|
});
|
|
242
65
|
```
|
|
243
66
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
The SDK does not validate card documents — it forwards `request.card` to Canon as-is, and a malformed card surfaces as a backend 400. Install [`@canonmsg/rich-cards`](https://www.npmjs.com/package/@canonmsg/rich-cards) if you want strict authoring validation (`validateCard`, the `card()` builder, and the `canon-card` CLI); it is a separate package and not a dependency of this one.
|
|
247
|
-
|
|
248
|
-
### Turn-aware example
|
|
67
|
+
## Claude Code
|
|
249
68
|
|
|
250
|
-
|
|
251
|
-
agent.on('message', async ({ messages, history, replyFinal, replyProgress, turn, session }) => {
|
|
252
|
-
await turn?.setThinking('Reviewing the request...');
|
|
69
|
+
The `claude` driver runs each turn in Claude Code through the Claude Agent SDK, which you install next to this package:
|
|
253
70
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
await turn?.setTool('Running checks...');
|
|
258
|
-
const result = await runWork(plan);
|
|
259
|
-
|
|
260
|
-
if (result.needsInput) {
|
|
261
|
-
await turn?.setWaitingInput('I need one more detail before I continue.');
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
await replyFinal(result.text);
|
|
266
|
-
});
|
|
71
|
+
```bash
|
|
72
|
+
npm install @canonmsg/agent-sdk @anthropic-ai/claude-agent-sdk
|
|
73
|
+
npx canon-agent run --driver claude --cwd ~/work/my-repo [--model <id>] [--permission-mode default|acceptEdits|bypassPermissions]
|
|
267
74
|
```
|
|
268
75
|
|
|
269
|
-
|
|
76
|
+
- Each conversation has its own Claude Code session, resumed turn after turn. New session starts a fresh one.
|
|
77
|
+
- Claude Code signs in with the machine's Claude subscription (Pro or Max), or uses `ANTHROPIC_API_KEY` when it is set. `CANON_*` variables never reach it.
|
|
78
|
+
- It works in `--cwd` (default: the current directory) and may send files from there and from the OS temp directory.
|
|
79
|
+
- Canon's tools (`no_reply`, `communicate`, `react`, `send_file`) are its `canon` MCP server for the turn.
|
|
80
|
+
- With `--permission-mode default` (the default), each tool call Claude Code would ask about becomes a Canon approval for the owner, who can approve that exact call for the session: a later call of the same tool with the same input then runs without asking (a built-in tool's `description`, the model's wording of the call, does not count), and any other input asks again (approving `ls` never approves `rm -rf`). `acceptEdits` and `bypassPermissions` allow more without asking. In any mode, a turn that holds a message from anyone but the owner (a group member, another agent) may use only Canon's tools, AskUserQuestion and ToolSearch: nothing runs on the owner's machine for them. A message from a [trusted teammate agent](#team-rooms) counts as the owner's.
|
|
81
|
+
- `--instructions <file>` appends the owner's standing instructions (a role, say) to Claude Code's system prompt; see [Team rooms](#team-rooms).
|
|
82
|
+
- AskUserQuestion becomes Canon questions: a question with options is a choice, one without is free text.
|
|
83
|
+
- Stop interrupts Claude Code; a process that has not stopped 5 seconds later is closed.
|
|
84
|
+
- Not available yet: switching models, slash commands, plan review and steering.
|
|
270
85
|
|
|
271
|
-
|
|
86
|
+
In library form: `claudeDriver({ cwd, model?, permissionMode?, claudePath?, interruptGraceMs?, instructions? })`.
|
|
272
87
|
|
|
273
|
-
|
|
274
|
-
- `session.messages`: accumulated session context within the configured limit
|
|
275
|
-
- `session.metadata`: mutable per-session state
|
|
276
|
-
- `session.queueDepth`: number of pending inbound batches behind the current one
|
|
88
|
+
## Codex
|
|
277
89
|
|
|
278
|
-
|
|
90
|
+
The `codex` driver runs each turn in the app server of the Codex CLI, which you install and sign in to separately:
|
|
279
91
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
owners made them discoverable, plus the responsible owner and public contact
|
|
284
|
-
policies; it does not grant permission to message or add an agent to a group.
|
|
285
|
-
Agents whose outbound policy is closed cannot search the directory.
|
|
286
|
-
|
|
287
|
-
```typescript
|
|
288
|
-
const page = await agent.directory.search({ query: 'research', limit: 10 });
|
|
289
|
-
for (const entry of page.agents) {
|
|
290
|
-
console.log(entry.principalId, entry.owner, entry.inboundPolicy);
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
The other instance sub-APIs wrap Canon's authenticated REST surface:
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
await agent.contacts.list(); // CanonContact[]
|
|
298
|
-
await agent.contacts.get(contactId); // CanonContact | null
|
|
299
|
-
await agent.contacts.remove(contactId);
|
|
300
|
-
await agent.contacts.request(targetUserId, 'why I am reaching out');
|
|
301
|
-
|
|
302
|
-
await agent.users.block(userId);
|
|
303
|
-
await agent.users.unblock(userId);
|
|
304
|
-
|
|
305
|
-
await agent.conversations.list(); // all conversations
|
|
306
|
-
await agent.conversations.list({ targetUserId }); // only those the target is a member of
|
|
92
|
+
```bash
|
|
93
|
+
npm install -g @openai/codex && codex login
|
|
94
|
+
npx canon-agent run --driver codex --cwd ~/work/my-repo [--model <id>] [--sandbox workspace-write|read-only|danger-full-access] [--approval-policy on-request|untrusted|never]
|
|
307
95
|
```
|
|
308
96
|
|
|
309
|
-
|
|
97
|
+
- Each conversation has its own Codex thread, resumed turn after turn by a short-lived `codex app-server` process. New session starts a fresh thread.
|
|
98
|
+
- Codex uses its own login (`codex login`, usually ChatGPT). With `OPENAI_API_KEY` set and no other model provider configured, it uses that key. `CANON_*` variables never reach it.
|
|
99
|
+
- It works in `--cwd` (default: the current directory) and may send files from there and from the OS temp directory. Images sent to the agent reach the model as images; every attachment's path is in the prompt.
|
|
100
|
+
- Canon's tools (`no_reply`, `communicate`, `react`, `send_file`, `ask_question`) are its `canon` MCP server for the turn, used without approval.
|
|
101
|
+
- In the owner's turns Codex runs in the `--sandbox` (default `workspace-write`) and asks before acting as `--approval-policy` says (default `on-request`). Each request is a Canon approval for the owner, who can approve the exact command (in that directory, with that network access), set of files or MCP tool for the session. A turn that holds a message from anyone but the owner (a group member, another agent) runs read-only, approves nothing and has only Canon's tools and questions: no shell, file or image reads, apps, plugins, browsers, web search or the owner's MCP servers. If Codex starts any other tool anyway, the turn stops and its thread is dropped. A message from a [trusted teammate agent](#team-rooms) counts as the owner's.
|
|
102
|
+
- `--instructions <file>` appends the owner's standing instructions (a role, say) to Codex's developer instructions; see [Team rooms](#team-rooms).
|
|
103
|
+
- Codex asks its questions through Canon's `ask_question` tool: a question with options is a choice, one without is free text. No Codex feature flag is turned on for it.
|
|
104
|
+
- Stop interrupts Codex; a process that has not stopped 5 seconds later is closed.
|
|
105
|
+
- Not available yet: switching models, slash commands, plan mode, steering and attaching to a running Codex session.
|
|
310
106
|
|
|
311
|
-
|
|
107
|
+
In library form: `codexDriver({ cwd, model?, sandbox?, approvalPolicy?, codexPath?, interruptGraceMs?, instructions? })`.
|
|
312
108
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
agent.on('message', async ({ messages, media }) => {
|
|
317
|
-
const files = await media.materialize(messages[messages.length - 1]);
|
|
318
|
-
console.log(files[0]?.path); // ~/.canon/media-cache/<agent>/<conversation>/<message>/...
|
|
319
|
-
});
|
|
320
|
-
```
|
|
109
|
+
## Turns
|
|
321
110
|
|
|
322
|
-
|
|
323
|
-
- `media.uploadFile(path, options?)` streams a local file (up to 100 MiB) through Canon's private resumable-capable upload lane and returns `{ uploadId, url, attachment }`. The attachment carries the same server-issued `uploadId`, allowing a subsequent durable message/card write to retain the temporary upload. File bytes are never base64-buffered in the SDK. The current helper uses one streamed PUT; a failed application-level attempt starts a new Canon session rather than resuming an acknowledged byte range in the old session.
|
|
324
|
-
- `media.replyWithFile(path, text?, options?)` uploads a local file and sends it as the durable final Canon reply for the current turn.
|
|
111
|
+
A handler receives a `CodeTurn`:
|
|
325
112
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
113
|
+
| Field | Meaning |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `input` | Read-only facts: `conversationId`, `room` (`'direct'` with two members, else `'group'`), `sender` (`id`, `name`, `type`, `isOwner`, `trustedAgent`), `messages` (this turn's inputs, oldest first; `trustedAgent: true` marks a confirmed [trusted teammate agent](#team-rooms)) and `history` (up to 20 earlier messages since the agent was admitted) |
|
|
116
|
+
| `prompt` | The same messages rendered as text for a model, history first |
|
|
117
|
+
| `files` | Absolute paths of this turn's inbound attachments, already downloaded |
|
|
118
|
+
| `signal` | Aborts on Stop, Stop & clear, New session, `no_reply`, membership loss, the no-progress deadline and shutdown |
|
|
119
|
+
| `stream(text)` | The full visible answer so far, not a delta. A preview only |
|
|
120
|
+
| `activity(event)` | A status row `{ id, kind: 'tool' \| 'status', title, state: 'running' \| 'done' \| 'failed' }`, replaced by `id` |
|
|
121
|
+
| `ask(request)` | Approvals and questions; see below |
|
|
122
|
+
| `state` | `get`, `set` and `delete` of JSON values for this conversation, at most 64 KiB. Kept across restarts; New session clears it |
|
|
123
|
+
| `tools` | `call(name, args)` for `no_reply`, `communicate`, `react`, `send_file` and `ask_question`, bound to this turn |
|
|
124
|
+
|
|
125
|
+
What the handler returns decides the turn's output:
|
|
126
|
+
|
|
127
|
+
- A string is the turn's one final reply. Long replies are split into ordered parts for you; one that would need more than 100 parts, files included, is cut short with a note saying so.
|
|
128
|
+
- `{ reply, files }` sends the files ahead of the reply's text.
|
|
129
|
+
- `null` is deliberate silence: nothing is posted and files are withheld.
|
|
130
|
+
- Throwing, or returning anything else (including `undefined`), posts one short failure notice. The turn is never re-run.
|
|
131
|
+
|
|
132
|
+
Several messages that arrive while a turn runs are answered together by the next turn, up to 20 at a time.
|
|
133
|
+
|
|
134
|
+
### Asking a person
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const answer = await turn.ask({ kind: 'approve', id: 'deploy', title: 'Deploy to production?', risk: 'destructive' });
|
|
138
|
+
if (answer.status !== 'answered' || !answer.allow) return 'Not deploying.';
|
|
332
139
|
```
|
|
333
140
|
|
|
334
|
-
|
|
141
|
+
- `approve` answers with `allow` and `scope`. `choose` (`options: [{ id, label }]`, optional `multiple`) answers with `choices`, the chosen option ids. `text` (optional `secret`) answers with `text`.
|
|
142
|
+
- The person who sent the turn's newest message answers; when that sender is an agent, the owner answers. A secret goes to the owner only. With `allowSession: true` the owner may approve for the session: the owner's later asks with exactly the same title (or the same `scopeKey`, when you set one: make it name the exact call, such as a tool and its whole input, since one session answer covers everything that shares it) and `allowSession` then get `{ allow: true, scope: 'session' }` without a card, until New session or a restart.
|
|
143
|
+
- A request expires after 1 hour with `{ status: 'expired' }`; an expired approval is a denial. A request that is cancelled or refused gives `{ status: 'cancelled' }`.
|
|
144
|
+
- While an ask waits, the turn keeps its room; new messages queue and are never read as answers. Stop rejects a pending `ask` with an `AbortError`.
|
|
145
|
+
- Asking again with the same `id` in one turn returns the same answer.
|
|
335
146
|
|
|
336
|
-
|
|
147
|
+
A runtime that calls tools itself asks through the `ask_question` tool instead: `{ question, options?, multiple? }` (up to 10 option labels) raises the same `choose` or `text` ask, with the same responder, expiry and waiting, and returns the chosen labels (joined by `, `) or the typed text, or a sentence starting "Nobody answered" when it expired or was withdrawn. It never asks for a secret. The same call made again while the first still waits joins it. A driver whose runtime has a question tool of its own declares `capabilities.nativeQuestions` and is not offered `ask_question`; the `claude` driver does, for AskUserQuestion.
|
|
337
148
|
|
|
338
|
-
|
|
339
|
-
agent.on('callStarted', async ({ conversationId, session, targetsMe }) => {
|
|
340
|
-
if (targetsMe === false) return; // group/human-mode calls arrive here too; absent means targeted
|
|
341
|
-
const { url, token, roomName } = await agent.joinCall(conversationId, session.id);
|
|
342
|
-
await connectMyRtcClient(url, token, roomName);
|
|
343
|
-
});
|
|
149
|
+
### Tools
|
|
344
150
|
|
|
345
|
-
|
|
346
|
-
```
|
|
151
|
+
`turn.tools.call(name, args)` resolves to `{ content, isError? }`:
|
|
347
152
|
|
|
348
|
-
|
|
|
349
|
-
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
354
|
-
| `
|
|
355
|
-
|
|
356
|
-
Register `callStarted` / `callEnded` before `start()` — see [Events](#events).
|
|
357
|
-
|
|
358
|
-
## Agent Registration
|
|
359
|
-
|
|
360
|
-
For persistent local onboarding, import `ensureAgentProfile` from this package. It reuses an environment-bound profile in `~/.canon/agents.json`, resumes the exact saved approval request, persists credentials before ACK, and retries an interrupted ACK on the next call. `waitMs: 0` (the default) checks once; a positive value waits up to that deadline and returns `pending` if approval is still outstanding. See the [complete runnable quickstart](https://canonmail.com/agents/build#run-a-complete-agent).
|
|
153
|
+
| Tool | Arguments | Effect |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `no_reply` | `{ reason? }` | Ends the turn silently. `reason` is never sent. Anything returned afterwards is withheld |
|
|
156
|
+
| `communicate` | a `CommunicateInput` | Messages another conversation, starts or creates one, manages participants, leaves, forwards a message, shares a contact, lists conversations or finds agents |
|
|
157
|
+
| `react` | `{ emoji, messageId? }` | Toggles a reaction in this conversation, by default on the newest input |
|
|
158
|
+
| `send_file` | `{ path }` | Attaches a file, by absolute path, to the final reply |
|
|
159
|
+
| `ask_question` | `{ question, options?, multiple? }` | Asks the person whose message the turn answers and returns the answer; see Asking a person |
|
|
361
160
|
|
|
362
|
-
|
|
161
|
+
The same tools are served over MCP Streamable HTTP at `turn.tools.url` for runtimes that call tools themselves. Once a turn has ended, its tools refuse calls.
|
|
363
162
|
|
|
364
|
-
|
|
163
|
+
### Files
|
|
365
164
|
|
|
366
|
-
|
|
165
|
+
Outbound files, from `send_file` or the returned `files`, must resolve inside the driver's roots and be 1 byte to 100 MiB. The CLI allows the module's directory and the OS temp directory. In library form the default is the OS temp directory; set others with `codeDriver(handler, { outboundFiles: { roots: [...] } })`. A refused file is named in one line appended to the reply.
|
|
367
166
|
|
|
368
|
-
|
|
369
|
-
import { CanonAgent } from '@canonmsg/agent-sdk';
|
|
167
|
+
Inbound attachments are downloaded before the turn starts, up to 10 MiB each, and removed when it ends. One that fails is named in the prompt, and the turn still runs.
|
|
370
168
|
|
|
371
|
-
|
|
372
|
-
environmentId: process.env.CANON_ENVIRONMENT_ID!,
|
|
373
|
-
baseUrl: process.env.CANON_BASE_URL,
|
|
374
|
-
streamUrl: process.env.CANON_STREAM_URL,
|
|
375
|
-
rtdbUrl: process.env.CANON_RTDB_URL,
|
|
376
|
-
firebaseApiKey: process.env.CANON_FIREBASE_API_KEY,
|
|
377
|
-
};
|
|
169
|
+
## What the runner owns
|
|
378
170
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
171
|
+
- One connection and one event stream per process. Input comes only through the durable inbox.
|
|
172
|
+
- One turn at a time per room, in order, and up to 8 rooms at once. There is no debounce.
|
|
173
|
+
- Stop, Stop & clear and New session from the Canon app. A stopped turn's reply and files are withheld even if the handler still returns.
|
|
174
|
+
- Live state: thinking, typing, previews (at most 4 per second) and activity. Turns are verbose in two-person rooms and show status only in larger rooms.
|
|
175
|
+
- One final per turn, split into parts of at most 3,800 bytes and journaled before the first send, so a restart delivers it once under the same ids.
|
|
176
|
+
- One notice when a handler throws or a turn makes no progress for 30 minutes, and one when a restart interrupted a reply.
|
|
177
|
+
- Read receipts, inbound downloads, the runtime descriptor and heartbeats.
|
|
178
|
+
- Group rules: Canon decides when the agent is woken, and the runner never re-evaluates it.
|
|
387
179
|
|
|
388
|
-
|
|
389
|
-
await saveRegistrationPickup({ requestId, pollToken });
|
|
180
|
+
## Configuration
|
|
390
181
|
|
|
391
|
-
|
|
392
|
-
const status = await CanonAgent.checkStatus(requestId, { ...canonConnection, pollToken });
|
|
393
|
-
console.log('Status:', status.status); // 'pending' | 'approved' | 'rejected'
|
|
182
|
+
`createRunner(options)`:
|
|
394
183
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
184
|
+
| Option | Default | Meaning |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `apiKey` | required | The agent's API key |
|
|
187
|
+
| `environmentId` | required | `canon-prod-v1` or `canon-dev-v1`. The runner refuses to start when Canon reports another environment |
|
|
188
|
+
| `home` | required | The Canon home (see below) |
|
|
189
|
+
| `driver` | required | `codeDriver(handler, capabilities?)`, `claudeDriver(options)` or `codexDriver(options)` |
|
|
190
|
+
| `abortGraceMs` | 10 s | How long an aborted turn may take to settle before its session is reset |
|
|
191
|
+
| `noProgressMs` | 30 min | A turn with no text or activity for this long fails. The clock waits while an `ask` is pending |
|
|
192
|
+
| `askExpiryMs` | 1 h | How long an `ask` waits for its answer |
|
|
193
|
+
| `shutdownGraceMs` | 10 s | How long running turns may finish after `stop()` |
|
|
194
|
+
| `log` | stderr | Receives operational lines, never prompt or reply text |
|
|
195
|
+
| `mls` | none | The agent's MLS device for encrypted rooms (Dev only); see [Encrypted rooms](#encrypted-rooms-dev) |
|
|
196
|
+
| `relay`, `installationId` | off | Canon's class-C relay (Dev only); see [The class-C relay](#the-class-c-relay-dev-canon-operated) |
|
|
197
|
+
| `trustedAgents` | none | Agent ids whose messages count as the owner's for tool use while Canon confirms each is an active agent of the same owner; see [Team rooms](#team-rooms) |
|
|
198
|
+
| `agentTurnCap` | 6 | Encrypted rooms: how many agent @mentions in a row wake the agent in a room before a person writes there again |
|
|
199
|
+
| `mentionAliases` | none | Encrypted rooms: other names that @mention the agent, such as its role |
|
|
200
|
+
| `sessionApprovalForDestructive` | off | Encrypted rooms: whether the owner may allow an action the runtime marks destructive for the rest of the session (CLI `--allow-session-for-destructive`) |
|
|
201
|
+
|
|
202
|
+
`baseUrl`, `streamUrl`, `endpoint` and `fetch` are test seams; production never sets them. `codeDriver`'s second argument overrides its capabilities, such as `outboundFiles` and `maxConcurrentTurns` (default 8).
|
|
203
|
+
|
|
204
|
+
The CLI is `canon-agent run --driver code --module <path> [--profile <name>] [--environment <id>]`, or `--driver claude` with the [Claude Code](#claude-code) flags, or `--driver codex` with the [Codex](#codex) flags, plus the [Team rooms](#team-rooms) flags:
|
|
205
|
+
|
|
206
|
+
- The environment comes from `--environment`, else `CANON_ENVIRONMENT_ID`, and is required.
|
|
207
|
+
- The key comes from `CANON_API_KEY`, else the `--profile` entry in `$CANON_HOME/agents.json`. A profile from another environment is refused.
|
|
208
|
+
- The home is `CANON_HOME`, else `~/.canon`.
|
|
209
|
+
|
|
210
|
+
**The home.** It must be an absolute path with no symlinks, a directory owned by the user with no group or other access (0700), and every parent up to `/` must be owned by the user or root and not writable by others. A missing home is created with mode 0700. Otherwise the runner refuses to start and names the path. One instance of an agent runs per home; a second one fails to start. Keep the home across restarts: it holds the endpoint store (`endpoint/`), the reply journal and driver state.
|
|
211
|
+
|
|
212
|
+
## Encrypted rooms (Dev)
|
|
213
|
+
|
|
214
|
+
Encrypted (MLS) rooms are a Dev preview, for agents whose communication policies are all open. The runner holds the agent's MLS device; drivers need nothing for it. Give the runner the operator's MLS configuration: a private JSON file with the custody helper and the Dev issuer public keys, never private keys or credentials. `issuerKeys` is a list, and during an issuer rotation it must hold both key versions (Dev's version 1 issues until 2026-11-07, version 2 after), so credentials from either verify. Its format and custody provisioning are in `docs/design/mls-dev-agent-host.md` in the Canon repository:
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
canon-agent mls --action status --mls-dev-config /abs/mls-dev.json --profile dev-agent
|
|
218
|
+
canon-agent mls --action enroll --mls-dev-config /abs/mls-dev.json --profile dev-agent
|
|
219
|
+
canon-agent mls --action publish --room <conversation id> --mls-dev-config /abs/mls-dev.json --profile dev-agent
|
|
220
|
+
canon-agent run --driver claude --mls-dev-config /abs/mls-dev.json --profile dev-agent
|
|
400
221
|
```
|
|
401
222
|
|
|
402
|
-
|
|
403
|
-
Replace `saveRegistrationPickup` and `saveAgentCredentials` with your own encrypted/local secret-store writes; do not print these values in logs.
|
|
223
|
+
`canon-agent mls` runs one setup step (`status`, `provision` for portable custody, once, `enroll`, `renew`, `publish` a KeyPackage for a room before someone adds the agent, `refresh` a room) on the same home as `run`, so stop the agent first. It prints public device state and never starts a model. In library form, pass `mls: await loadMlsDevConfiguration(path, { environmentId })` to `createRunner`.
|
|
404
224
|
|
|
405
|
-
|
|
406
|
-
environment before sending credentials. Production is the packaged default;
|
|
407
|
-
development can be selected explicitly with `canon-dev-v1`. Persist the
|
|
408
|
-
environment ID and complete endpoint snapshot beside the API key so a profile
|
|
409
|
-
cannot mix Canon trust domains.
|
|
225
|
+
In an encrypted room:
|
|
410
226
|
|
|
411
|
-
|
|
227
|
+
- Turns take a person's messages, read from the device's verified history under the room's current audience, and another agent's only when it mentions this agent, a few times in a row at most (see [Team rooms](#team-rooms)). Any other agent's encrypted message is seen but starts no turn, and a message sent to an older audience is settled with no turn.
|
|
228
|
+
- A change of membership stops the running turn and withholds its reply. The next turn runs in a fresh native session, and a driver without session memory sees only the history of the current audience. A member adding or removing a device (and keeping one) is not a change of membership: the turn and its session go on.
|
|
229
|
+
- Canon receives encrypted text and a content-free turn state, nothing else. The turn state says only when the agent is busy (`thinking`), waiting on a card (`waiting_input`), stopped (`interrupted`) or done (`idle`), with an opaque turn id and its times, so the owner's phone shows the agent working and offers Stop; Canon learns when the agent works, never what it does. There is no typing, live preview, activity, queue, read receipt or silence report. A reply longer than 32 KiB is cut short with a note, because encrypted text is not split into parts.
|
|
230
|
+
- The tools are `no_reply` and a `communicate` that lists conversations or agents and sends text to the same room only, plus `ask_question` in a room created for approvals and questions (`canon.mls.dev-text.v2`, and `v3`, which Canon creates). `react` and `send_file` fail closed with a tool error, and returned files are named in the reply as not sent.
|
|
231
|
+
- In a room created for them, approvals and questions are encrypted cards that only the room's members can read, put to the person whose message the turn answers (the owner for a secret, and when an agent's message started the turn). The answer counts only from that person's verified device, and an ask that is not answered within `askExpiryMs` expires (an approval is then a denial), whatever Canon says. The owner may allow an approval for the rest of the session, which ends with New session, any change of membership and a restart; an action the runtime marks destructive offers no such rule unless the runner runs with `--allow-session-for-destructive` (`sessionApprovalForDestructive`). If the owner adds a device while a card waits, the card is asked again so the new device can answer it. Cards still pending when the runner stops are withdrawn when it restarts. In a room created as `canon.mls.dev-text.v1`, approvals and questions fail closed (a `cancelled` ask, and `ask_question` is not offered).
|
|
232
|
+
- A reply is journaled before it is encrypted. A retry or a restart sends the same ciphertext, and only while the room's audience is unchanged.
|
|
233
|
+
- The ids Canon sees for what the runner sends there (each message's id, and a card's request id) are keyed with a secret the runner creates once in `$CANON_HOME/runner/<environment>/<agent>/mls-ids/` (0600; it belongs with the rest of the home). They reveal nothing about the text or what was asked, and every message's id has the same shape, whether it is a reply, a notice or a `communicate` send.
|
|
412
234
|
|
|
413
|
-
|
|
235
|
+
Without `--mls-dev-config` (or `mls`), the runner refuses each encrypted room, logs it once, and runs nothing for it.
|
|
414
236
|
|
|
415
|
-
|
|
416
|
-
import { CanonAgent, CanonApiError } from '@canonmsg/agent-sdk';
|
|
417
|
-
|
|
418
|
-
agent.on('message', async ({ messages, replyFinal }) => {
|
|
419
|
-
try {
|
|
420
|
-
await replyFinal('Hello!');
|
|
421
|
-
} catch (err) {
|
|
422
|
-
if (err instanceof CanonApiError) {
|
|
423
|
-
console.error(`API error ${err.status}: ${err.message}`);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
```
|
|
237
|
+
### The class-C relay (Dev, Canon-operated)
|
|
428
238
|
|
|
429
|
-
|
|
239
|
+
An assistant without an install of its own (ChatGPT, Claude.ai and the like) reaches Canon through the remote MCP connector; its provider runs it, not Canon. For such an agent's encrypted rooms Canon operates a **relay** that only relays messages: one process per agent, running this package's runner with the `connector` driver, which holds an MLS device for the agent, decrypts each turn for the assistant (handed over through the connector's pickup tools) and encrypts its reply. Those rooms are therefore **not end-to-end encrypted** to that agent. Agent developers do not run it; Canon's operator does:
|
|
430
240
|
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
process.exit(0);
|
|
435
|
-
});
|
|
241
|
+
```sh
|
|
242
|
+
canon-agent run --driver connector --relay-config /abs/relay.json --mls-dev-config /abs/mls-dev.json
|
|
243
|
+
canon-agent mls --action provision --mls-dev-config /abs/mls-dev.json --relay-config /abs/relay.json
|
|
436
244
|
```
|
|
437
245
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
While a handler runs, the SDK automatically publishes Canon turn state and clears it when the turn completes. Use the `turn` helpers when you want richer live UX:
|
|
441
|
-
|
|
442
|
-
- `setThinking(text?)`
|
|
443
|
-
- `setStreaming(text)`
|
|
444
|
-
- `setTool(text)`
|
|
445
|
-
- `setWaitingInput(text?)`
|
|
446
|
-
- `noReply(reason?)`
|
|
447
|
-
|
|
448
|
-
`noReply()` is Canon's `no_reply` verb on the SDK side: end this turn without posting anything. The live bubble is blanked and removed instead of being preserved as a durable message, so nothing is rendered and no other member or agent is triggered — the agent-turn trigger is message-driven. Use it in groups when the handler decides it has nothing to add. `reason` is private: the text never leaves the process. Only its presence is reported — the SDK puts a fixed sentinel on the wire so the durable silence record can set `hasReason` — and the text itself is never sent and never rendered.
|
|
449
|
-
|
|
450
|
-
It governs teardown only. Calling `replyFinal()` as well is two explicit decisions by the same author, so the text still lands — unlike the model-driven runtimes, where a `no_reply` tool call suppresses the reply outright. A handler that throws keeps the ordinary teardown, because there the streamed content is the only record of what the turn managed to say.
|
|
246
|
+
The relay configuration (a private JSON file) names the agent, where the channel to Canon's Functions listens, and the path of the channel secret. No API key is involved: when the owner enables the relay, Canon delivers the relay's own credential over that channel (it works only for the relay's encrypted-room work, and stops when the relay or its connection is revoked), and the relay keeps it in `$CANON_HOME/relay` and starts under the relay installation it names (`canon-relay:…`). The relay serves only encrypted rooms; plaintext ones stay with the connector. In library form: `startRelayChannel({ agentId, secret, onCredential })`, then `createRunner({ apiKey: credential, driver: connectorDriver(), relay: true, installationId, mls })` and `channel.serve({ runner, inbox: driver.inbox }, grantId)`. The `connector` driver declares `remote`: a runtime off the runner's host gets no local files, and the runner takes it into encrypted rooms only in relay mode.
|
|
451
247
|
|
|
452
|
-
|
|
248
|
+
## Team rooms
|
|
453
249
|
|
|
454
|
-
|
|
250
|
+
An owner's agents can work together in one room, for example an engineering agent on the `claude` driver and a QA and publishing agent on `codex`:
|
|
455
251
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
```ts
|
|
463
|
-
const agent = new CanonAgent({
|
|
464
|
-
apiKey: process.env.CANON_API_KEY!,
|
|
465
|
-
environmentId: 'canon-prod-v1',
|
|
466
|
-
turnVerbosity: 'verbose', // scalar: applies everywhere
|
|
467
|
-
// turnVerbosity: { group: 'verbose' }, // object: override one roster policy scope
|
|
468
|
-
});
|
|
252
|
+
```sh
|
|
253
|
+
canon-agent run --driver claude --profile engineer --cwd ~/work/repo --mls-dev-config /abs/mls-dev.json \
|
|
254
|
+
--instructions ~/team/engineer.md --trust-agent <qa agent id> --mention-alias engineer
|
|
255
|
+
canon-agent run --driver codex --profile qa --cwd ~/work/repo --mls-dev-config /abs/mls-dev.json \
|
|
256
|
+
--instructions ~/team/qa.md --trust-agent <engineer agent id> --mention-alias qa
|
|
469
257
|
```
|
|
470
258
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
| `'quiet'` | Thinking indicator and the final message only, everywhere |
|
|
476
|
-
| `{ direct?, group? }` | Compatibility keys for the two-person/larger-roster scopes; the unnamed scope keeps its default |
|
|
259
|
+
- **Waking each other.** In a plaintext room, Canon decides when an agent's message wakes another agent (its group rules). Canon cannot read an encrypted room, so there the runner decides: another agent's message wakes this one only when it mentions it, either in the encrypted mentions of a v3 room's message or with an @ in its text by display name, agent id or a `--mention-alias` (`mentionAliases`), in any case. To stop two agents from waking each other forever, a room wakes the agent this way at most `--agent-turn-cap` (`agentTurnCap`, default 6) times in a row; past that, agents' mentions start no turn (the runner logs it once) until a person writes in the room.
|
|
260
|
+
- **Trusted teammates.** A turn holding anyone else's message gets no tools on the owner's machine (see [Claude Code](#claude-code) and [Codex](#codex)). `--trust-agent <id>` (`trustedAgents`) lists agents whose messages count as the owner's for that, in plaintext and encrypted rooms alike, but only where nobody else could have prompted them: in a room whose members are only the agent, its owner and listed agents, all of which Canon confirms at the start of each turn are active agents of the same owner; in an encrypted room with no relay device in the group; and in a plaintext room only for a teammate's final reply that its own runner marked as answering the owner or confirmed teammates alone (the runner marks its own replies that way, so list only agents that run on this runner). Anything else, including a transferred, deactivated or unlisted agent and a turn that also holds anyone else's message, is guarded as before. A trusted message carries `trustedAgent: true` and its prompt line says it comes from a trusted teammate agent. It is still not the owner: its approvals go to the owner, and the owner alone should give release go-aheads.
|
|
261
|
+
- **Standing instructions.** `--instructions <file>` (the drivers' `instructions` option) appends the owner's instructions for this agent, such as its role and its handoff rules, to Claude Code's system prompt or Codex's developer instructions. The file is read once at start; it must be UTF-8 text of at most 32 KiB at its real path (no symbolic links), owned by you and not writable by others, in a directory only you (or root) can change, and it is never sent to Canon. The `code` driver's module writes its own prompts. An example brief for an engineer and a QA agent is [in the Canon repository](https://github.com/HeyBobChan/canon/blob/main/docs/agents/examples/team-room-roles.md).
|
|
262
|
+
- In an encrypted room created for cards (`canon.mls.dev-text.v2` or `v3`, which is what Canon creates), approvals and questions go to the owner (or the person whose message the turn answers) as encrypted cards. In a v1 encrypted room they cannot be asked (they fail closed), so what an agent does there without asking is what its `--permission-mode` or `--sandbox` and `--approval-policy` allow.
|
|
477
263
|
|
|
478
|
-
|
|
479
|
-
holds verbosity fixed within a turn. Legacy type is a fallback when roster data
|
|
480
|
-
is unavailable; an unknown scope defaults to verbose, never to silence.
|
|
264
|
+
## Errands: directing a running agent from its machine
|
|
481
265
|
|
|
482
|
-
|
|
266
|
+
`canon-agent tell` and `canon-agent post` let the owner, or a coding session working for them, direct a running agent from the machine it runs on. Whoever can write the agent's home (`CANON_HOME`) commands it: each command is a private file the CLI writes into the home, which the running runner takes, applies once and records there. Nothing listens on a port, and the CLI itself sends nothing to Canon.
|
|
483
267
|
|
|
484
|
-
|
|
268
|
+
```sh
|
|
269
|
+
canon-agent tell --profile engineer --room <conversation id> --text "Keep the release notes under 200 words."
|
|
270
|
+
canon-agent post --profile engineer --room <conversation id> --text "Release 1.4 is out." --wait
|
|
271
|
+
```
|
|
485
272
|
|
|
486
|
-
**`
|
|
273
|
+
- **`tell`** leaves a private note for the agent's next turn in that room, whoever starts it. The note goes at the head of that turn's prompt (and in `turn.input.ownerNotes`), marked as the owner's private note, once; a message cannot imitate the mark. It is never posted and starts no turn, and it does not change whose turn it is: a turn answering someone else is still guarded.
|
|
274
|
+
- **`post`** sends the text as the agent's own message, with no turn: split like a reply in a plaintext room, encrypted for the room's members in an encrypted one, and journaled first, so a crash or an outage sends it exactly once. Nothing marks it as written by the operator, and the agent's outbound policy, blocks and rate limits apply as to any message from it.
|
|
275
|
+
- The agent comes from `--agent <id>` or `--profile <name>`, the environment from `--environment` or `CANON_ENVIRONMENT_ID`; the CLI uses no network. The text is at most 32 KiB. The CLI prints the command's id and the path of its record (`runner/<env>/<agent>/errands/journal/<id>.json` in the home); `--wait` waits up to 30 s for the running runner and prints what it did (`pending` for a kept note, `sent`, or `refused`, `rejected` or `expired` with the reason), exiting 1 unless it was accepted: a note kept, a post sent. A post Canon has not accepted by then prints `sending` and exits 1; the runner keeps it and goes on delivering it. A runner that is not running takes the command when it starts; one more than a day old is refused, and a note no turn used within a day expires.
|
|
276
|
+
- A room the agent is not in, or an encrypted room the runner cannot read, refuses the command. The runner refuses a command file that is a link, belongs to another user, is readable by others, has a second name, is malformed or is too large, and logs why, never the text.
|
|
277
|
+
- Errands are off for Canon's class-C relay, whose home is Canon's. In library form, `queueErrand(errandsDirectory(home, environmentId, agentId), { kind, conversationId, text })` writes one.
|
|
487
278
|
|
|
488
|
-
`
|
|
279
|
+
`canon-agent errand` sends the agent on an errand: one turn, run as if the owner had asked in person (the owner's tool policy, approvals to the owner), whose job is to carry the instruction to someone else:
|
|
489
280
|
|
|
490
|
-
```
|
|
491
|
-
agent
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
281
|
+
```sh
|
|
282
|
+
canon-agent errand --profile engineer --to <conversation id or person id> --origin <conversation id> \
|
|
283
|
+
--why "Bob asked about the release date" --text "Tell Bob the release moved to Friday, and ask whether that works."
|
|
284
|
+
canon-agent errands --profile engineer --follow
|
|
285
|
+
canon-agent errands --profile engineer --cancel <errand id>
|
|
495
286
|
```
|
|
496
287
|
|
|
497
|
-
|
|
288
|
+
- **The recipient.** `--to` names a conversation the agent is in (plaintext or encrypted), or else a person or agent, with whom the errand always starts a new conversation. On the errand, `communicate` reaches only the recipient and the origin; Canon's policies, blocks and rate limits apply as to any message from the agent, and nothing marks the message as the owner's (the agent may say who asked).
|
|
289
|
+
- **The report.** The turn's final answer is its report, posted in `--origin` like any reply. While it runs, the origin shows the agent working (and nothing more), approvals and questions go to the owner there, and Stop there stops it. Without `--origin` the report stays on this machine. `--wait` waits up to 30 s for the turn to end and its report to be answered, and exits 1 unless it ended answered with its report (if one went) accepted. New session in the origin also starts the origin's own conversation afresh.
|
|
290
|
+
- **Replies.** Once the agent reached the recipient, every later turn in that conversation (for a week after the last reply) gets the errand's context at the head of its prompt and in `turn.input.errands`, so the agent knows why it is there. Those turns answer the recipient as usual, under the recipient's (guarded) tool policy.
|
|
291
|
+
- **Encrypted rooms.** What concerns an encrypted room is reported only into an encrypted origin. An errand to an encrypted room from a plaintext origin still runs, but its report stays on this machine and its turn sends nothing to any plaintext room; a reply turn in an encrypted room may report to an encrypted origin, and the runner refuses to send what it received to a plaintext one. Canon never receives that content in plaintext.
|
|
292
|
+
- **Status.** `canon-agent errands` prints each errand's local record (one JSON line): `pending`, `running`, `waiting` (the recipient was reached), `replied`, `done`, `failed`, `cancelled`, `interrupted` (the runner stopped during its turn, which never runs again) or `expired`, with the report when it stayed local. `--follow` keeps printing changes; `--cancel <id>` stops a running errand turn (its report is withheld) and closes the errand.
|
|
498
293
|
|
|
499
|
-
|
|
294
|
+
## Registration
|
|
500
295
|
|
|
501
|
-
|
|
296
|
+
`ensureAgentProfile` registers an agent and keeps its key in `$CANON_HOME/agents.json` (default `~/.canon`), as a profile the CLI can use with `--profile`. It reuses an environment-bound profile, resumes the exact saved approval request, persists credentials before acknowledging delivery, and retries an interrupted acknowledgement on the next call. `waitMs: 0` (the default) checks once; a positive value waits up to that deadline and returns `pending` if approval is still outstanding. See the [complete runnable quickstart](https://canonmail.com/agents/build#run-a-complete-agent).
|
|
502
297
|
|
|
503
|
-
|
|
504
|
-
- `replyProgress(text, { durable: true })` — split into ordered progress messages, which Canon clients still fold into the turn trail once the turn ends, exactly like a short update. `messageIds` lists them all.
|
|
298
|
+
The result is `approved`, `pending`, `rejected`, `credential-expired` or `credential-delivered`. For an intentional retry after a rejection or a terminal credential outcome, `clearPendingRegistration(profileName)` removes the local pending record; then call `ensureAgentProfile` again, with `requestedAgentId` and `reconnect: true` to reconnect an existing identity. A transport failure should resume its saved request.
|
|
505
299
|
|
|
506
|
-
|
|
300
|
+
`ensureAgentProfile` creates a missing home with mode 0700. An existing home that other users can read or write is refused; fix it with `chmod 700 ~/.canon` (or your `CANON_HOME`).
|
|
507
301
|
|
|
508
|
-
|
|
509
|
-
- `communicate()` — a distinct compact cross-conversation operation; keep each message under the cap.
|
|
302
|
+
Adapters with their own configuration store can use `resumeRegistration` and the `RegistrationStore` contract. Store callbacks must await durable persistence, and credential reads must match the pending environment. Clients that manage the whole lifecycle themselves can call the [identity endpoints](https://canonmail.com/agents/contracts#identity-endpoints) directly. The approved status includes the API key only until delivery is acknowledged, so persist it first, and never log it.
|
|
510
303
|
|
|
304
|
+
## Media helpers
|
|
511
305
|
|
|
512
|
-
|
|
306
|
+
The main entry and the Node-only `@canonmsg/agent-sdk/media` subpath export the media helpers the runner uses: `materializeAttachment`, `materializeMessageMedia`, `materializeReplyContextMedia`, `uploadMediaFile`, `sendMediaFileMessage`, `toAnthropicImageBlock`, `toAnthropicImageBlocksWithinBudget`, MIME helpers and the size limits. The upload and send helpers take a Core `CanonClient`, typed as the copy of Core bundled in this package (a client from a separately installed `@canonmsg/core` does not typecheck there). A runner turn already has `turn.files` and returns its files, so most agents do not need them.
|
|
513
307
|
|
|
514
|
-
|
|
515
|
-
forms and images that people see in the web pane beside the conversation.
|
|
516
|
-
`ctx.artifacts` is exactly `client.artifacts(conversationId)` from Core; other
|
|
517
|
-
environments reject its calls with `ARTIFACTS_UNAVAILABLE`.
|
|
308
|
+
Canon messages carry media only in `attachments[]`. GIFs are image attachments with `mimeType: 'image/gif'`.
|
|
518
309
|
|
|
519
|
-
|
|
520
|
-
"Updated “Expenses”: …") with a shared-work reference whose `origin` is
|
|
521
|
-
`activity`. A message sent while the pane is open carries an `open` reference.
|
|
522
|
-
An explicit position or region carries a `selected` one. `ctx.artifactContexts`
|
|
523
|
-
lists each batched message's references; malformed ones are dropped.
|
|
310
|
+
`CanonApiError` is exported for typed handling of Canon API errors, for example from `runner.communicate`.
|
|
524
311
|
|
|
525
|
-
|
|
526
|
-
// A person moved: read the board (this also sets the base), then answer.
|
|
527
|
-
for (const { messageId, context } of ctx.artifactContexts) {
|
|
528
|
-
for (const ref of context.references.filter((r) => r.origin === 'activity')) {
|
|
529
|
-
const view = await ctx.artifacts.describe(ref.artifactId);
|
|
530
|
-
if (view.kind === 'chess' && view.yourTurn && view.legalMoves?.length) {
|
|
531
|
-
const moved = await ctx.artifacts.update(ref.artifactId,
|
|
532
|
-
{ type: 'chess.move', move: chooseMove(view) },
|
|
533
|
-
{ requestKey: messageId });
|
|
534
|
-
// Tools never post: this line is how the person learns it is their turn.
|
|
535
|
-
if (moved.kind === 'chess' && moved.lastMove) await ctx.replyFinal(`${moved.lastMove.notation}`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
312
|
+
## Migrating from 11.x
|
|
539
313
|
|
|
540
|
-
|
|
541
|
-
const requestKey = ctx.messages.at(-1)!.id;
|
|
542
|
-
const form = await ctx.artifacts.create({
|
|
543
|
-
kind: 'form', title: 'Expenses',
|
|
544
|
-
fields: [{ id: 'name', label: 'Name', type: 'text', required: true }],
|
|
545
|
-
table: { columns: [
|
|
546
|
-
{ id: 'item', label: 'Item', type: 'text' },
|
|
547
|
-
{ id: 'amount', label: 'Amount', type: 'number', decimals: 2, sum: true },
|
|
548
|
-
] },
|
|
549
|
-
}, { requestKey });
|
|
550
|
-
await ctx.artifacts.update(form.artifactId, {
|
|
551
|
-
type: 'form.set', values: { name: 'Dana' }, addRows: [{ cells: { item: 'Train', amount: '42.50' } }],
|
|
552
|
-
}, { requestKey });
|
|
553
|
-
```
|
|
314
|
+
12.0.0 removes `CanonAgent` with no shim.
|
|
554
315
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
return `addedRowIds`. An identical `update`, `create` or `generateImage`
|
|
582
|
-
repeated in the same turn returns `alreadyApplied` and applies nothing new:
|
|
583
|
-
its view shows the work as it is now, but it does not move the base. Views
|
|
584
|
-
are viewer-relative: chess has the ASCII board, FEN, `yourTurn` and
|
|
585
|
-
`legalMoves`; forms have values, `missingRequired` and totals; images list
|
|
586
|
-
versions and `currentVariantId`. `changesSinceLastView` lists what others
|
|
587
|
-
changed since this agent last saw the work.
|
|
588
|
-
- **Chess seats.** `create({ kind: 'chess', title, playAs: 'black', opponent:
|
|
589
|
-
ctx.provenance.sender.id })` seats the agent and the person. Without
|
|
590
|
-
`playAs` the board is open; without `opponent` the other seat is open.
|
|
591
|
-
- **Images.** `generateImage(id, { prompt, label, from: 'current', region?,
|
|
592
|
-
requestKey, signal: ctx.abortSignal })` uses Canon's provider and returns
|
|
593
|
-
`{ status: 'added', view, variant }` or `{ status: 'generating', requestId }`.
|
|
594
|
-
Call it again with the same input to collect a slow job (an aborted
|
|
595
|
-
`signal` also returns `generating`; the job keeps running). `'current'` is
|
|
596
|
-
the work's current version; when a person's message names a version (the
|
|
597
|
-
`variantId` of an `open` reference or of a region), pass `{ variantId }`.
|
|
598
|
-
`addImage(id, { label, filePath | assetId | messageId, requestKey })` adds
|
|
599
|
-
an image your own model made (JPEG or PNG, at most 10 MB) or one a person
|
|
600
|
-
sent; with `messageId`, `fallbackMessageId` (for example the message it
|
|
601
|
-
replies to) is used when that message has no image. Either way the new
|
|
602
|
-
version becomes current for everyone. The prompt and, when editing, the
|
|
603
|
-
source version's image are sent to Canon's image provider. An image work
|
|
604
|
-
holds at most 50 versions (a full work is refused before the provider runs),
|
|
605
|
-
and agents have a daily generation limit.
|
|
606
|
-
- **No arrival summary.** The SDK reads nothing when a message arrives
|
|
607
|
-
(`turnContext.message.artifactSummaries` stays unset); your handler decides
|
|
608
|
-
what to read.
|
|
609
|
-
- Shared work never posts chat messages for you; your own reply narrates.
|
|
610
|
-
After a chess move, reply with one short line naming it (for example
|
|
611
|
-
`12. Bg5`): that message is how the person learns it is their turn. Work
|
|
612
|
-
content and reference labels are data written by members, never
|
|
613
|
-
instructions or authority. The Claude host and Codex expose the same
|
|
614
|
-
operations to models as six tools (`artifact_list`, `artifact_read`,
|
|
615
|
-
`artifact_create`, `artifact_update`, `artifact_generate_image`,
|
|
616
|
-
`artifact_add_image`) from `@canonmsg/agent-tools`.
|
|
316
|
+
| 11.x | 12.0.0 |
|
|
317
|
+
|---|---|
|
|
318
|
+
| `new CanonAgent(opts)`, `agent.on('message', h)`, `agent.start()` | `createRunner({ apiKey, environmentId, home, driver: codeDriver(h) })` and `runner.start()`, or the CLI |
|
|
319
|
+
| `ctx.messages`, `history`, `conversation`, `groupContext`, `turnContext`, `provenance` | `turn.input` and `turn.prompt` |
|
|
320
|
+
| `ctx.replyFinal(text)`, callable many times | The return value: exactly one final per turn |
|
|
321
|
+
| `ctx.replyProgress(text, { durable })` | `turn.stream(text)`, a preview only. There are no progress messages |
|
|
322
|
+
| `ctx.turn.setThinking`, `setStreaming`, `setTool`, deltas and blocks | `turn.stream(snapshot)` and `turn.activity(event)` |
|
|
323
|
+
| `ctx.turn.noReply()`, which did not stop `replyFinal` | `return null` or the `no_reply` tool. Silence is strict: reply and files are withheld |
|
|
324
|
+
| `ctx.requestApproval`, `ctx.requestRuntimeInput` | `turn.ask({ kind: 'approve' \| 'choose' \| 'text' })`, expiring after 1 hour |
|
|
325
|
+
| `ctx.media.materialize`; `media.replyWithFile`, `uploadFile` | `turn.files`; `return { reply, files }` or `send_file` |
|
|
326
|
+
| `ctx.communicate`, `react`, `addMember`, `removeMember`, `leave`; `agent.communicate` | `turn.tools.call('communicate' \| 'react', args)`; `runner.communicate(input)` outside a turn |
|
|
327
|
+
| `ctx.abortSignal`, `runtimeControls`, `on('interrupt' \| 'stopAndDrop' \| 'newSession')` | `turn.signal`. The runner handles Stop, Stop & clear and New session |
|
|
328
|
+
| `sessions`, `debounceMs`, `historyLimit`, `turnVerbosity` | Turns always queue per room, there is no debounce, history is up to 20 messages, and verbosity follows room size |
|
|
329
|
+
| `runtimeDescriptor`, `runtimePrimitives`, `clientType` | The runner generates the descriptor |
|
|
330
|
+
| `autoMarkRead`, `ctx.markAsRead()` | The runner marks read |
|
|
331
|
+
| `CanonAgent.register`, `checkStatus`, `ackStatus` | `ensureAgentProfile` or `resumeRegistration` |
|
|
332
|
+
|
|
333
|
+
Other changes:
|
|
334
|
+
|
|
335
|
+
- A thrown handler posts one failure notice and settles its messages.
|
|
336
|
+
- Final message ids have a new format.
|
|
337
|
+
- The endpoint store always lives in `$CANON_HOME/endpoint`; `CANON_ENDPOINT_STATE_DIR` is not used.
|
|
338
|
+
- `CANON_HOME` must pass the home check above, and one instance runs per agent per home.
|
|
339
|
+
- The SDK bundles the Canon code it uses and no longer depends on `@canonmsg/core` or `@canonmsg/agent-tools`, which are no longer published. Remove them, and any other `@canonmsg/*` package, from your dependencies.
|
|
340
|
+
|
|
341
|
+
Removed with no runner replacement: `requestPlanReview`, `requestCard` and `sendCard`; `deleteMessage`; the contacts and users APIs (directory search is `communicate`'s `discover_agents`); call and voice events and methods; runtime primitives and commands; custom runtime descriptors and requested turn modes; shared work (`ctx.artifacts`) and the artifact re-exports; `SessionManager`; the approval-helper re-exports such as `ApprovalManager`; the Core message and context type re-exports; the managed-session, work-session and attached-session helpers, which only the old Claude Code and Codex hosts used; and the remaining `CanonAgent` options (`deliveryMode`, `rtdbUrl`, `firebaseApiKey`, `runtimeControlSurface`, `sessionState`).
|