@canonmsg/agent-sdk 8.2.0 → 8.3.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 +1 -1
- package/dist/canon-agent.js +46 -5
- package/dist/realtime.d.ts +8 -1
- package/dist/realtime.js +12 -0
- package/dist/types.d.ts +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -476,7 +476,7 @@ While a handler runs, the SDK automatically publishes Canon turn state and clear
|
|
|
476
476
|
- `setWaitingInput(text?)`
|
|
477
477
|
- `noReply(reason?)`
|
|
478
478
|
|
|
479
|
-
`noReply()` is Canon's `no_reply` verb on the SDK side: end this turn without posting anything. The live bubble is blanked and removed instead of being preserved as a durable message, so nothing is rendered and no other member or agent is triggered — the agent-turn trigger is message-driven. Use it in groups when the handler decides it has nothing to add. `reason` is private: the SDK
|
|
479
|
+
`noReply()` is Canon's `no_reply` verb on the SDK side: end this turn without posting anything. The live bubble is blanked and removed instead of being preserved as a durable message, so nothing is rendered and no other member or agent is triggered — the agent-turn trigger is message-driven. Use it in groups when the handler decides it has nothing to add. `reason` is private: the text never leaves the process. Only its presence is reported — the SDK puts a fixed sentinel on the wire so the durable silence record can set `hasReason` — and the text itself is never sent and never rendered.
|
|
480
480
|
|
|
481
481
|
It governs teardown only. Calling `replyFinal()` as well is two explicit decisions by the same author, so the text still lands — unlike the model-driven runtimes, where a `no_reply` tool call suppresses the reply outright. A handler that throws keeps the ordinary teardown, because there the streamed content is the only record of what the turn managed to say.
|
|
482
482
|
|
package/dist/canon-agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reachOutToCanonContact, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
1
|
+
import { ApprovalManager, RuntimeRequestManager, runtimeInputDescriptor, runtimeCardDescriptor, CanonClient, ControlChannelPoller, buildCanonTurnContextV2, buildCanonGroupContext, buildParticipationHistorySnapshot, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, diffCanonMemberIds, FINAL_MESSAGE_HANDOFF_MS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, buildRuntimeInputOutcome, initRTDBAuth, isChunkedSendMessageError, normalizeRuntimeCommandDescriptors, normalizeTurnMetadata, normalizeTurnVerbosityConversationType, reachOutToCanonContact, reportNoReplyOutcome, resolveCanonReplyContext, resolveMessageActiveSelfContextId, resolveRuntimeProvenance, resolveTurnVerbosity, selectActiveSelfContexts, renderCanonHostInboundContent, resolveCanonRuntimeConnection, sendMessageWithRetryChunked, shouldPublishTurnTrail, splitTextByUtf8Bytes, verifyCanonRuntimeConnection, } from '@canonmsg/core';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { AuthManager } from './auth.js';
|
|
4
4
|
import { Debouncer } from './debouncer.js';
|
|
@@ -1252,11 +1252,18 @@ export class CanonAgent {
|
|
|
1252
1252
|
// Governs TEARDOWN only — `replyFinal` still sends, because in the SDK the
|
|
1253
1253
|
// developer's two explicit calls are two explicit decisions.
|
|
1254
1254
|
let deliberatelySilent = false;
|
|
1255
|
+
// The FIRST noReply call's reason (one silence decision per turn), carried
|
|
1256
|
+
// to teardown where the outcome report fires. Only its presence rides the
|
|
1257
|
+
// wire — see reportNoReplyOutcome's sentinel.
|
|
1258
|
+
let noReplyReason;
|
|
1255
1259
|
// Set when the handler threw or the turn was aborted. Deliberate silence
|
|
1256
1260
|
// must never blank the live node on those paths: the streamed content is
|
|
1257
1261
|
// all that survives a turn that died mid-flight.
|
|
1258
1262
|
let turnEndedAbnormally = false;
|
|
1259
1263
|
let durableMessageSequence = 0;
|
|
1264
|
+
// The freshest message in the batch is the turn's trigger — same convention
|
|
1265
|
+
// as the provenance lookup for turn verbosity below.
|
|
1266
|
+
const triggeringMessageId = messages[messages.length - 1]?.id;
|
|
1260
1267
|
const agentId = this.agentId;
|
|
1261
1268
|
const runtimeState = this.createRuntimeStatePublisher();
|
|
1262
1269
|
const queueDepth = () => this.sessionManager?.getQueueDepth(conversationId) ?? 0;
|
|
@@ -1309,7 +1316,7 @@ export class CanonAgent {
|
|
|
1309
1316
|
//
|
|
1310
1317
|
// Re-deriving it mid-turn is what must never happen: a turn that started
|
|
1311
1318
|
// quiet and finished verbose would emit a trail nothing narrated.
|
|
1312
|
-
const inboundConversationType = normalizeTurnVerbosityConversationType(provenanceByMessageId?.get(
|
|
1319
|
+
const inboundConversationType = normalizeTurnVerbosityConversationType(provenanceByMessageId?.get(triggeringMessageId ?? '')?.conversation?.type);
|
|
1313
1320
|
const turnVerbosity = resolveTurnVerbosity({
|
|
1314
1321
|
configured: selectConfiguredTurnVerbosity(this.options.turnVerbosity, inboundConversationType),
|
|
1315
1322
|
conversationType: inboundConversationType,
|
|
@@ -1468,6 +1475,10 @@ export class CanonAgent {
|
|
|
1468
1475
|
return { turnId, durable: false, messageId: null };
|
|
1469
1476
|
}
|
|
1470
1477
|
throwIfAborted();
|
|
1478
|
+
// Durable progress skips `sendDurableMessage` (see the chunking note
|
|
1479
|
+
// below) but it is still a durable message in the timeline, so it must
|
|
1480
|
+
// advance the same counter teardown reads for the silence report.
|
|
1481
|
+
durableMessageSequence += 1;
|
|
1471
1482
|
const { durable: _durable, ...sendOptions } = options;
|
|
1472
1483
|
const sendOptionsWithContext = withActiveSelfContext(sendOptions);
|
|
1473
1484
|
// Progress text is caller-controlled too, so it can also blow past
|
|
@@ -1554,8 +1565,13 @@ export class CanonAgent {
|
|
|
1554
1565
|
// untouched metadata, so the common case (and the interim→final handoff
|
|
1555
1566
|
// that keys on that id) is unchanged.
|
|
1556
1567
|
const sendDurableMessage = async (text, options, fallbackMessageIdParts) => {
|
|
1568
|
+
// Counts every durable send the turn attempts, not just the ones that
|
|
1569
|
+
// needed a generated id: teardown reads `durableMessageSequence` to
|
|
1570
|
+
// decide whether the turn genuinely ended silent, and a reply sent
|
|
1571
|
+
// under an explicit caller messageId must not look like silence.
|
|
1572
|
+
durableMessageSequence += 1;
|
|
1557
1573
|
const messageId = options?.messageId
|
|
1558
|
-
?? buildSdkMessageId([...fallbackMessageIdParts, durableMessageSequence
|
|
1574
|
+
?? buildSdkMessageId([...fallbackMessageIdParts, durableMessageSequence]);
|
|
1559
1575
|
const { messageIds } = await sendMessageWithRetryChunked(abortAwareClient, conversationId, text, {
|
|
1560
1576
|
...(options ?? {}),
|
|
1561
1577
|
messageId,
|
|
@@ -2022,6 +2038,7 @@ export class CanonAgent {
|
|
|
2022
2038
|
...(options?.mimeType ? { mimeType: options.mimeType } : {}),
|
|
2023
2039
|
...(options?.durationMs != null ? { durationMs: options.durationMs } : {}),
|
|
2024
2040
|
});
|
|
2041
|
+
durableMessageSequence += 1;
|
|
2025
2042
|
await sleep(FINAL_MESSAGE_HANDOFF_MS);
|
|
2026
2043
|
return result;
|
|
2027
2044
|
}
|
|
@@ -2146,11 +2163,19 @@ export class CanonAgent {
|
|
|
2146
2163
|
},
|
|
2147
2164
|
noReply: async (reason) => {
|
|
2148
2165
|
throwIfAborted();
|
|
2166
|
+
// First call wins — one silence decision per turn.
|
|
2167
|
+
if (!deliberatelySilent && reason?.trim()) {
|
|
2168
|
+
noReplyReason = reason;
|
|
2169
|
+
}
|
|
2149
2170
|
deliberatelySilent = true;
|
|
2150
|
-
// `reason` is handler-authored free text:
|
|
2151
|
-
// given, never the text itself.
|
|
2171
|
+
// `reason` is handler-authored free text: log only that one was given.
|
|
2152
2172
|
console.error(`[canon-sdk] Turn chose no_reply for ${conversationId}`
|
|
2153
2173
|
+ ` (reason: ${reason?.trim() ? 'given' : 'none'})`);
|
|
2174
|
+
// The outcome report deliberately does NOT fire here: `replyFinal`
|
|
2175
|
+
// still sends after `noReply` (the SDK carve-out — see
|
|
2176
|
+
// TurnController.noReply in types.ts) and the handler may yet throw
|
|
2177
|
+
// or be aborted, so only teardown knows whether the silence was
|
|
2178
|
+
// real. It fires there, once, off the flag set above.
|
|
2154
2179
|
},
|
|
2155
2180
|
setTool: async (text) => {
|
|
2156
2181
|
await writeTurn('tool');
|
|
@@ -2224,6 +2249,22 @@ export class CanonAgent {
|
|
|
2224
2249
|
// here there is nothing to hand off to, and holding the indicator would
|
|
2225
2250
|
// read as "started to answer, then gave up".
|
|
2226
2251
|
const silentTeardown = deliberatelySilent && !turnEndedAbnormally;
|
|
2252
|
+
// Telemetry parity with the server-answered verb path, decided HERE
|
|
2253
|
+
// because only teardown knows how the turn actually ended: `noReply`
|
|
2254
|
+
// does not gate `replyFinal` in the SDK, and the handler may throw or be
|
|
2255
|
+
// aborted after choosing silence — either would make a report sent at
|
|
2256
|
+
// noReply-call time record silence for a turn that delivered or died.
|
|
2257
|
+
// `durableMessageSequence` counts the turn's durable sends: any durable
|
|
2258
|
+
// message means the turn did not end silent, deliberate flag or not.
|
|
2259
|
+
// reportNoReplyOutcome is fire-and-forget and never throws, so a failed
|
|
2260
|
+
// or rejecting report degrades to exactly the pre-report behavior.
|
|
2261
|
+
if (silentTeardown && durableMessageSequence === 0) {
|
|
2262
|
+
reportNoReplyOutcome(this.apiClient, {
|
|
2263
|
+
conversationId,
|
|
2264
|
+
...(triggeringMessageId ? { messageId: triggeringMessageId } : {}),
|
|
2265
|
+
...(noReplyReason ? { reason: noReplyReason } : {}),
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2227
2268
|
// Always clear typing when done
|
|
2228
2269
|
try {
|
|
2229
2270
|
await this.typingSignals.clear(conversationId);
|
package/dist/realtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentContext, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload, type VoiceSessionEventPayload } from '@canonmsg/core';
|
|
1
|
+
import { type AgentContext, type ContactAddedPayload, type ContactApprovedPayload, type ContactRemovedPayload, type ContactRequestPayload, type ConversationUpdatedPayload, type MessageUpdatedPayload, type ParticipationSuppressedPayload, type VoiceSessionEventPayload } from '@canonmsg/core';
|
|
2
2
|
import { Debouncer } from './debouncer.js';
|
|
3
3
|
/**
|
|
4
4
|
* Wraps @canonmsg/core's CanonStream with SDK-specific features:
|
|
@@ -20,6 +20,7 @@ export declare class RealtimeManager {
|
|
|
20
20
|
private onContactAdded;
|
|
21
21
|
private onContactRemoved;
|
|
22
22
|
private onConversationUpdated;
|
|
23
|
+
private onParticipationSuppressed;
|
|
23
24
|
private onMessageUpdated;
|
|
24
25
|
private onMessageDeleted;
|
|
25
26
|
private onConnected;
|
|
@@ -43,6 +44,12 @@ export declare class RealtimeManager {
|
|
|
43
44
|
onContactRemoved?: (payload: ContactRemovedPayload) => void;
|
|
44
45
|
}): void;
|
|
45
46
|
setConversationUpdatedHandler(cb: (payload: ConversationUpdatedPayload) => void): void;
|
|
47
|
+
/**
|
|
48
|
+
* Observe-only notice that Canon's participation gate withheld a turn this
|
|
49
|
+
* agent would otherwise have been dispatched. Never run a turn off it — it
|
|
50
|
+
* exists so an agent can tell deliberate policy from a dead stream.
|
|
51
|
+
*/
|
|
52
|
+
setParticipationSuppressedHandler(cb: (payload: ParticipationSuppressedPayload) => void): void;
|
|
46
53
|
setMessageUpdatedHandler(cb: (payload: MessageUpdatedPayload) => void): void;
|
|
47
54
|
setMessageDeletedHandler(cb: (payload: {
|
|
48
55
|
conversationId: string;
|
package/dist/realtime.js
CHANGED
|
@@ -21,6 +21,7 @@ export class RealtimeManager {
|
|
|
21
21
|
onContactAdded = null;
|
|
22
22
|
onContactRemoved = null;
|
|
23
23
|
onConversationUpdated = null;
|
|
24
|
+
onParticipationSuppressed = null;
|
|
24
25
|
onMessageUpdated = null;
|
|
25
26
|
onMessageDeleted = null;
|
|
26
27
|
onConnected = null;
|
|
@@ -113,6 +114,9 @@ export class RealtimeManager {
|
|
|
113
114
|
onConversationUpdated: (payload) => {
|
|
114
115
|
this.onConversationUpdated?.(payload);
|
|
115
116
|
},
|
|
117
|
+
onParticipationSuppressed: (payload) => {
|
|
118
|
+
this.onParticipationSuppressed?.(payload);
|
|
119
|
+
},
|
|
116
120
|
onConnected: () => {
|
|
117
121
|
this.onConnected?.();
|
|
118
122
|
},
|
|
@@ -180,6 +184,14 @@ export class RealtimeManager {
|
|
|
180
184
|
setConversationUpdatedHandler(cb) {
|
|
181
185
|
this.onConversationUpdated = cb;
|
|
182
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Observe-only notice that Canon's participation gate withheld a turn this
|
|
189
|
+
* agent would otherwise have been dispatched. Never run a turn off it — it
|
|
190
|
+
* exists so an agent can tell deliberate policy from a dead stream.
|
|
191
|
+
*/
|
|
192
|
+
setParticipationSuppressedHandler(cb) {
|
|
193
|
+
this.onParticipationSuppressed = cb;
|
|
194
|
+
}
|
|
183
195
|
setMessageUpdatedHandler(cb) {
|
|
184
196
|
this.onMessageUpdated = cb;
|
|
185
197
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -68,7 +68,9 @@ export interface TurnController {
|
|
|
68
68
|
* message, so nothing is rendered and no other member or agent is triggered —
|
|
69
69
|
* the agent-turn trigger is message-driven.
|
|
70
70
|
*
|
|
71
|
-
* `reason` is private:
|
|
71
|
+
* `reason` is private: the text never leaves the process. Only its presence
|
|
72
|
+
* is reported (a fixed sentinel on the wire), so the durable silence record
|
|
73
|
+
* can set `hasReason` — the text itself is never sent and never rendered.
|
|
72
74
|
*
|
|
73
75
|
* Unlike the model-driven runtimes, this does NOT gate `replyFinal`. Calling
|
|
74
76
|
* both is two explicit decisions by the same author, so the text still lands;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@canonmsg/core": "^10.
|
|
31
|
+
"@canonmsg/core": "^10.2.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|