@canonmsg/agent-sdk 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canon-agent.d.ts +17 -3
- package/dist/canon-agent.js +312 -8
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +17 -2
- package/package.json +2 -2
package/dist/canon-agent.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type AddMemberResult, type CanonContact, type ContactCardPayload, type CreateContactRequestResult } from '@canonmsg/core';
|
|
2
|
-
import type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, RuntimeSignalHandler } from './types.js';
|
|
1
|
+
import { type AddMemberResult, type CanonContact, type CanonRuntimeActivityItem, type CanonRuntimeCommandDescriptor, type CanonRuntimeFact, type CanonRuntimePrimitiveId, type ContactCardPayload, type ClearRuntimeActivityOptions, type CreateContactRequestResult } from '@canonmsg/core';
|
|
2
|
+
import type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, CreateConversationOptions, MessageHandler, ReachOutOptions, ReachOutResult, ContactRequestHandler, RuntimeSignalHandler, RuntimePrimitiveHandler } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Contact-graph operations exposed under `agent.contacts`. Wraps the REST
|
|
5
5
|
* endpoints in CanonClient — the same surface a human user would hit through
|
|
@@ -33,6 +33,8 @@ export declare class CanonAgent {
|
|
|
33
33
|
private interruptHandler;
|
|
34
34
|
private stopAndDropHandler;
|
|
35
35
|
private newSessionHandler;
|
|
36
|
+
private readonly primitiveHandlers;
|
|
37
|
+
private primitiveFallbackHandler;
|
|
36
38
|
/** Contact-graph operations (`agent.contacts.*`). Initialized in the constructor. */
|
|
37
39
|
readonly contacts: AgentContactsAPI;
|
|
38
40
|
/** Block/unblock operations (`agent.users.*`). Initialized in the constructor. */
|
|
@@ -45,6 +47,7 @@ export declare class CanonAgent {
|
|
|
45
47
|
private runtimeHeartbeatTimer;
|
|
46
48
|
private runtimeControlPollTimer;
|
|
47
49
|
private readonly lastSeenSignal;
|
|
50
|
+
private readonly primitiveRequestDedupe;
|
|
48
51
|
private readonly activeAbortControllers;
|
|
49
52
|
private readonly conversationMemberIds;
|
|
50
53
|
private readonly pendingMembershipChanges;
|
|
@@ -57,6 +60,11 @@ export declare class CanonAgent {
|
|
|
57
60
|
on(event: 'interrupt', handler: RuntimeSignalHandler): void;
|
|
58
61
|
on(event: 'stopAndDrop', handler: RuntimeSignalHandler): void;
|
|
59
62
|
on(event: 'newSession', handler: RuntimeSignalHandler): void;
|
|
63
|
+
onPrimitive(primitive: CanonRuntimePrimitiveId | '*', handler: RuntimePrimitiveHandler): void;
|
|
64
|
+
describeCommands(_provider?: string): ReadonlyArray<CanonRuntimeCommandDescriptor>;
|
|
65
|
+
publishRuntimeFacts(conversationId: string, facts: ReadonlyArray<CanonRuntimeFact>): Promise<void>;
|
|
66
|
+
publishRuntimeActivity(conversationId: string, item: CanonRuntimeActivityItem): Promise<void>;
|
|
67
|
+
clearRuntimeActivity(conversationId: string, options?: ClearRuntimeActivityOptions): Promise<void>;
|
|
60
68
|
/**
|
|
61
69
|
* Resolve admission live for a target user (typically read off a shared
|
|
62
70
|
* contact card) and route into either an immediate message or a contact
|
|
@@ -100,6 +108,8 @@ export declare class CanonAgent {
|
|
|
100
108
|
private hasStopAndDropSupport;
|
|
101
109
|
private hasNewSessionSupport;
|
|
102
110
|
private hasRuntimeSignalSupport;
|
|
111
|
+
private hasRuntimePrimitiveSupport;
|
|
112
|
+
private hasRuntimeControlSupport;
|
|
103
113
|
private buildRuntimeDescriptor;
|
|
104
114
|
private buildRuntimeCapabilities;
|
|
105
115
|
private publishAgentRuntime;
|
|
@@ -113,12 +123,16 @@ export declare class CanonAgent {
|
|
|
113
123
|
private baselineRuntimeControlSignals;
|
|
114
124
|
private startRuntimeControlPolling;
|
|
115
125
|
private stopRuntimeControlPolling;
|
|
116
|
-
private
|
|
126
|
+
private pollRuntimeControls;
|
|
127
|
+
private handleRuntimePrimitiveRequests;
|
|
128
|
+
private clearRuntimePrimitiveRequest;
|
|
129
|
+
private prunePrimitiveRequestDedupe;
|
|
117
130
|
private handleRuntimeSignal;
|
|
118
131
|
private abortActiveTurns;
|
|
119
132
|
private resolveBatchDeliveryIntent;
|
|
120
133
|
private notifyMessageInterrupt;
|
|
121
134
|
private createRuntimeStatePublisher;
|
|
135
|
+
private requireRuntimeStatePublisher;
|
|
122
136
|
private handleMessages;
|
|
123
137
|
private executeHandler;
|
|
124
138
|
static register(options: {
|
package/dist/canon-agent.js
CHANGED
|
@@ -5,6 +5,8 @@ import { Debouncer } from './debouncer.js';
|
|
|
5
5
|
import { materializeMessageMedia, sendMediaFileMessage, uploadMediaFile, } from './media.js';
|
|
6
6
|
import { SessionManager } from './session-manager.js';
|
|
7
7
|
const AGENT_RUNTIME_HEARTBEAT_MS = 30_000;
|
|
8
|
+
const RUNTIME_PRIMITIVE_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
9
|
+
const RUNTIME_PRIMITIVE_DEDUPE_MAX = 1_000;
|
|
8
10
|
const SDK_RUNTIME_CAPABILITIES = {
|
|
9
11
|
supportsInterrupt: false,
|
|
10
12
|
supportsQueue: true,
|
|
@@ -18,9 +20,154 @@ const DEFAULT_SDK_RUNTIME_DESCRIPTOR = {
|
|
|
18
20
|
supportsInterrupt: false,
|
|
19
21
|
streamingTextMode: 'snapshot',
|
|
20
22
|
};
|
|
23
|
+
const STANDARD_PRIMITIVE_COMMANDS = {
|
|
24
|
+
'runtime.status': {
|
|
25
|
+
id: 'runtime-status',
|
|
26
|
+
label: 'Runtime status',
|
|
27
|
+
description: 'Ask the runtime for its current status.',
|
|
28
|
+
primitive: 'runtime.status',
|
|
29
|
+
aliases: ['status'],
|
|
30
|
+
category: 'runtime',
|
|
31
|
+
placements: ['composer_slash', 'command_palette'],
|
|
32
|
+
availability: ['always'],
|
|
33
|
+
dispatch: { kind: 'primitive', primitive: 'runtime.status' },
|
|
34
|
+
},
|
|
35
|
+
'runtime.reasoning.set': {
|
|
36
|
+
id: 'thinking-level',
|
|
37
|
+
label: 'Thinking level',
|
|
38
|
+
description: 'Set the runtime reasoning or effort level.',
|
|
39
|
+
primitive: 'runtime.reasoning.set',
|
|
40
|
+
aliases: ['think', 'effort'],
|
|
41
|
+
category: 'runtime',
|
|
42
|
+
placements: ['composer_slash', 'command_palette'],
|
|
43
|
+
availability: ['always'],
|
|
44
|
+
args: [{
|
|
45
|
+
id: 'level',
|
|
46
|
+
label: 'Level',
|
|
47
|
+
kind: 'enum',
|
|
48
|
+
required: true,
|
|
49
|
+
choices: [
|
|
50
|
+
{ value: 'low', label: 'Low' },
|
|
51
|
+
{ value: 'medium', label: 'Medium' },
|
|
52
|
+
{ value: 'high', label: 'High' },
|
|
53
|
+
],
|
|
54
|
+
}],
|
|
55
|
+
dispatch: { kind: 'primitive', primitive: 'runtime.reasoning.set' },
|
|
56
|
+
},
|
|
57
|
+
'runtime.verbosity.set': {
|
|
58
|
+
id: 'verbosity',
|
|
59
|
+
label: 'Verbosity',
|
|
60
|
+
description: 'Set runtime verbosity.',
|
|
61
|
+
primitive: 'runtime.verbosity.set',
|
|
62
|
+
aliases: ['verbose'],
|
|
63
|
+
category: 'runtime',
|
|
64
|
+
placements: ['composer_slash', 'command_palette'],
|
|
65
|
+
availability: ['always'],
|
|
66
|
+
args: [{
|
|
67
|
+
id: 'level',
|
|
68
|
+
label: 'Level',
|
|
69
|
+
kind: 'enum',
|
|
70
|
+
required: true,
|
|
71
|
+
choices: [
|
|
72
|
+
{ value: 'off', label: 'Off' },
|
|
73
|
+
{ value: 'on', label: 'On' },
|
|
74
|
+
{ value: 'full', label: 'Full' },
|
|
75
|
+
],
|
|
76
|
+
}],
|
|
77
|
+
dispatch: { kind: 'primitive', primitive: 'runtime.verbosity.set' },
|
|
78
|
+
},
|
|
79
|
+
'runtime.usage': {
|
|
80
|
+
id: 'usage',
|
|
81
|
+
label: 'Usage',
|
|
82
|
+
description: 'Show or change runtime usage reporting.',
|
|
83
|
+
primitive: 'runtime.usage',
|
|
84
|
+
aliases: ['usage'],
|
|
85
|
+
category: 'runtime',
|
|
86
|
+
placements: ['composer_slash', 'command_palette'],
|
|
87
|
+
availability: ['always'],
|
|
88
|
+
dispatch: { kind: 'primitive', primitive: 'runtime.usage' },
|
|
89
|
+
},
|
|
90
|
+
'context.compact': {
|
|
91
|
+
id: 'compact-context',
|
|
92
|
+
label: 'Compact context',
|
|
93
|
+
description: 'Ask the runtime to compact its conversation context.',
|
|
94
|
+
primitive: 'context.compact',
|
|
95
|
+
aliases: ['compact'],
|
|
96
|
+
category: 'session',
|
|
97
|
+
placements: ['composer_slash', 'command_palette'],
|
|
98
|
+
availability: ['always'],
|
|
99
|
+
dispatch: { kind: 'primitive', primitive: 'context.compact' },
|
|
100
|
+
},
|
|
101
|
+
'session.new': {
|
|
102
|
+
id: 'new-session-primitive',
|
|
103
|
+
label: 'New session',
|
|
104
|
+
description: 'Ask the runtime to start a fresh session.',
|
|
105
|
+
primitive: 'session.new',
|
|
106
|
+
aliases: ['new'],
|
|
107
|
+
category: 'session',
|
|
108
|
+
placements: ['composer_slash', 'command_palette', 'session_strip'],
|
|
109
|
+
availability: ['always'],
|
|
110
|
+
dispatch: { kind: 'primitive', primitive: 'session.new' },
|
|
111
|
+
},
|
|
112
|
+
'session.reset': {
|
|
113
|
+
id: 'reset-session',
|
|
114
|
+
label: 'Reset session',
|
|
115
|
+
description: 'Ask the runtime to reset the current session.',
|
|
116
|
+
primitive: 'session.reset',
|
|
117
|
+
aliases: ['reset'],
|
|
118
|
+
category: 'session',
|
|
119
|
+
placements: ['composer_slash', 'command_palette'],
|
|
120
|
+
availability: ['always'],
|
|
121
|
+
dispatch: { kind: 'primitive', primitive: 'session.reset' },
|
|
122
|
+
},
|
|
123
|
+
};
|
|
21
124
|
function sleep(ms) {
|
|
22
125
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
126
|
}
|
|
127
|
+
function isRuntimePrimitiveId(value) {
|
|
128
|
+
return value === 'runtime.status'
|
|
129
|
+
|| value === 'runtime.reasoning.set'
|
|
130
|
+
|| value === 'runtime.verbosity.set'
|
|
131
|
+
|| value === 'runtime.usage'
|
|
132
|
+
|| value === 'context.compact'
|
|
133
|
+
|| value === 'session.new'
|
|
134
|
+
|| value === 'session.reset';
|
|
135
|
+
}
|
|
136
|
+
function normalizePrimitiveArgs(value) {
|
|
137
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
const args = {};
|
|
141
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
142
|
+
if (!/^[A-Za-z0-9_-]+$/.test(key))
|
|
143
|
+
continue;
|
|
144
|
+
if (typeof rawValue === 'boolean' || typeof rawValue === 'string') {
|
|
145
|
+
args[key] = rawValue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return args;
|
|
149
|
+
}
|
|
150
|
+
function normalizeRuntimeFact(fact) {
|
|
151
|
+
const id = fact.id.trim();
|
|
152
|
+
const label = fact.label.trim();
|
|
153
|
+
const value = fact.value.trim();
|
|
154
|
+
if (!id || !label || !value)
|
|
155
|
+
return null;
|
|
156
|
+
return {
|
|
157
|
+
...fact,
|
|
158
|
+
id,
|
|
159
|
+
label,
|
|
160
|
+
value,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function normalizeRuntimeActivityItem(item) {
|
|
164
|
+
return {
|
|
165
|
+
...item,
|
|
166
|
+
id: item.id.trim(),
|
|
167
|
+
title: item.title.trim() || item.kind,
|
|
168
|
+
updatedAt: item.updatedAt || Date.now(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
24
171
|
export class CanonAgent {
|
|
25
172
|
options;
|
|
26
173
|
apiClient;
|
|
@@ -36,6 +183,8 @@ export class CanonAgent {
|
|
|
36
183
|
interruptHandler = null;
|
|
37
184
|
stopAndDropHandler = null;
|
|
38
185
|
newSessionHandler = null;
|
|
186
|
+
primitiveHandlers = new Map();
|
|
187
|
+
primitiveFallbackHandler = null;
|
|
39
188
|
/** Contact-graph operations (`agent.contacts.*`). Initialized in the constructor. */
|
|
40
189
|
contacts;
|
|
41
190
|
/** Block/unblock operations (`agent.users.*`). Initialized in the constructor. */
|
|
@@ -48,6 +197,7 @@ export class CanonAgent {
|
|
|
48
197
|
runtimeHeartbeatTimer = null;
|
|
49
198
|
runtimeControlPollTimer = null;
|
|
50
199
|
lastSeenSignal = new Map();
|
|
200
|
+
primitiveRequestDedupe = new Map();
|
|
51
201
|
activeAbortControllers = new Map();
|
|
52
202
|
conversationMemberIds = new Map();
|
|
53
203
|
pendingMembershipChanges = new Map();
|
|
@@ -84,6 +234,14 @@ export class CanonAgent {
|
|
|
84
234
|
this.interruptHandler = options.runtimeControls?.onInterrupt ?? null;
|
|
85
235
|
this.stopAndDropHandler = options.runtimeControls?.onStopAndDrop ?? null;
|
|
86
236
|
this.newSessionHandler = options.runtimeControls?.onNewSession ?? null;
|
|
237
|
+
for (const [primitive, handler] of Object.entries(options.runtimePrimitives ?? {})) {
|
|
238
|
+
if (primitive === '*') {
|
|
239
|
+
this.primitiveFallbackHandler = handler;
|
|
240
|
+
}
|
|
241
|
+
else if (isRuntimePrimitiveId(primitive)) {
|
|
242
|
+
this.primitiveHandlers.set(primitive, handler);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
87
245
|
}
|
|
88
246
|
on(event, handler) {
|
|
89
247
|
if (event === 'message') {
|
|
@@ -134,6 +292,44 @@ export class CanonAgent {
|
|
|
134
292
|
}
|
|
135
293
|
this.contactRemovedHandler = handler;
|
|
136
294
|
}
|
|
295
|
+
onPrimitive(primitive, handler) {
|
|
296
|
+
if (primitive === '*') {
|
|
297
|
+
this.primitiveFallbackHandler = handler;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
this.primitiveHandlers.set(primitive, handler);
|
|
301
|
+
}
|
|
302
|
+
if (this.running) {
|
|
303
|
+
this.startRuntimeControlPolling();
|
|
304
|
+
}
|
|
305
|
+
void this.publishAgentRuntime().catch(() => { });
|
|
306
|
+
}
|
|
307
|
+
describeCommands(_provider) {
|
|
308
|
+
return this.buildRuntimeDescriptor().commands ?? [];
|
|
309
|
+
}
|
|
310
|
+
async publishRuntimeFacts(conversationId, facts) {
|
|
311
|
+
this.rememberConversationId(conversationId);
|
|
312
|
+
const publisher = this.requireRuntimeStatePublisher();
|
|
313
|
+
const normalizedFacts = facts
|
|
314
|
+
.map(normalizeRuntimeFact)
|
|
315
|
+
.filter((fact) => Boolean(fact));
|
|
316
|
+
await publisher.patchRuntimeInfo(conversationId, {
|
|
317
|
+
descriptor: this.buildRuntimeDescriptor(),
|
|
318
|
+
facts: normalizedFacts,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async publishRuntimeActivity(conversationId, item) {
|
|
322
|
+
this.rememberConversationId(conversationId);
|
|
323
|
+
const normalized = normalizeRuntimeActivityItem(item);
|
|
324
|
+
if (!normalized.id) {
|
|
325
|
+
throw new Error('Runtime activity item id is required.');
|
|
326
|
+
}
|
|
327
|
+
await this.requireRuntimeStatePublisher().writeRuntimeActivity(conversationId, normalized);
|
|
328
|
+
}
|
|
329
|
+
async clearRuntimeActivity(conversationId, options) {
|
|
330
|
+
this.rememberConversationId(conversationId);
|
|
331
|
+
await this.requireRuntimeStatePublisher().clearRuntimeActivity(conversationId, options);
|
|
332
|
+
}
|
|
137
333
|
/**
|
|
138
334
|
* Resolve admission live for a target user (typically read off a shared
|
|
139
335
|
* contact card) and route into either an immediate message or a contact
|
|
@@ -371,6 +567,12 @@ export class CanonAgent {
|
|
|
371
567
|
hasRuntimeSignalSupport() {
|
|
372
568
|
return this.hasInterruptSupport() || this.hasStopAndDropSupport() || this.hasNewSessionSupport();
|
|
373
569
|
}
|
|
570
|
+
hasRuntimePrimitiveSupport() {
|
|
571
|
+
return this.primitiveHandlers.size > 0 || Boolean(this.primitiveFallbackHandler);
|
|
572
|
+
}
|
|
573
|
+
hasRuntimeControlSupport() {
|
|
574
|
+
return this.hasRuntimeSignalSupport() || this.hasRuntimePrimitiveSupport();
|
|
575
|
+
}
|
|
374
576
|
buildRuntimeDescriptor() {
|
|
375
577
|
const source = this.options.runtimeDescriptor ?? DEFAULT_SDK_RUNTIME_DESCRIPTOR;
|
|
376
578
|
const hasInterrupt = this.hasInterruptSupport();
|
|
@@ -399,9 +601,23 @@ export class CanonAgent {
|
|
|
399
601
|
if (hasNewSession && !hasNewSessionAction) {
|
|
400
602
|
actions.push(RUNTIME_NEW_SESSION_ACTION);
|
|
401
603
|
}
|
|
604
|
+
const commands = [...(source.commands ?? [])].filter((command) => {
|
|
605
|
+
if (command.dispatch.kind !== 'primitive')
|
|
606
|
+
return true;
|
|
607
|
+
return this.primitiveHandlers.has(command.dispatch.primitive)
|
|
608
|
+
|| Boolean(this.primitiveFallbackHandler);
|
|
609
|
+
});
|
|
610
|
+
const hasCommandForPrimitive = (primitive) => commands.some((command) => (command.primitive === primitive
|
|
611
|
+
|| (command.dispatch.kind === 'primitive' && command.dispatch.primitive === primitive)));
|
|
612
|
+
for (const primitive of this.primitiveHandlers.keys()) {
|
|
613
|
+
if (!hasCommandForPrimitive(primitive)) {
|
|
614
|
+
commands.push(STANDARD_PRIMITIVE_COMMANDS[primitive]);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
402
617
|
return {
|
|
403
618
|
...source,
|
|
404
619
|
supportsInterrupt: hasInterrupt,
|
|
620
|
+
commands,
|
|
405
621
|
actions,
|
|
406
622
|
};
|
|
407
623
|
}
|
|
@@ -493,10 +709,10 @@ export class CanonAgent {
|
|
|
493
709
|
}));
|
|
494
710
|
}
|
|
495
711
|
startRuntimeControlPolling() {
|
|
496
|
-
if (!this.agentId || this.runtimeControlPollTimer || !this.
|
|
712
|
+
if (!this.agentId || this.runtimeControlPollTimer || !this.hasRuntimeControlSupport())
|
|
497
713
|
return;
|
|
498
714
|
this.runtimeControlPollTimer = setInterval(() => {
|
|
499
|
-
void this.
|
|
715
|
+
void this.pollRuntimeControls();
|
|
500
716
|
}, 2_000);
|
|
501
717
|
this.runtimeControlPollTimer.unref?.();
|
|
502
718
|
}
|
|
@@ -506,16 +722,97 @@ export class CanonAgent {
|
|
|
506
722
|
clearInterval(this.runtimeControlPollTimer);
|
|
507
723
|
this.runtimeControlPollTimer = null;
|
|
508
724
|
}
|
|
509
|
-
async
|
|
510
|
-
if (!this.agentId || !this.
|
|
725
|
+
async pollRuntimeControls() {
|
|
726
|
+
if (!this.agentId || !this.hasRuntimeControlSupport())
|
|
511
727
|
return;
|
|
512
728
|
await Promise.all(this.cachedConversationIds.map(async (conversationId) => {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
729
|
+
if (this.hasRuntimeSignalSupport()) {
|
|
730
|
+
const raw = await Promise.resolve(rtdbRead(`/control/${conversationId}/${this.agentId}/signal`)).catch(() => null);
|
|
731
|
+
if (raw && typeof raw === 'object') {
|
|
732
|
+
await this.handleRuntimeSignal(conversationId, raw);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (this.hasRuntimePrimitiveSupport()) {
|
|
736
|
+
const raw = await Promise.resolve(rtdbRead(`/control/${conversationId}/${this.agentId}/primitive`)).catch(() => null);
|
|
737
|
+
if (raw && typeof raw === 'object') {
|
|
738
|
+
await this.handleRuntimePrimitiveRequests(conversationId, raw);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
517
741
|
}));
|
|
518
742
|
}
|
|
743
|
+
async handleRuntimePrimitiveRequests(conversationId, raw) {
|
|
744
|
+
if (!this.agentId)
|
|
745
|
+
return;
|
|
746
|
+
this.prunePrimitiveRequestDedupe();
|
|
747
|
+
const requests = Object.entries(raw)
|
|
748
|
+
.map(([requestId, value]) => ({ requestId, value }))
|
|
749
|
+
.filter((entry) => (Boolean(entry.requestId)
|
|
750
|
+
&& Boolean(entry.value)
|
|
751
|
+
&& typeof entry.value === 'object'
|
|
752
|
+
&& !Array.isArray(entry.value)))
|
|
753
|
+
.sort((a, b) => Number(a.value.updatedAt ?? 0) - Number(b.value.updatedAt ?? 0));
|
|
754
|
+
for (const { requestId, value } of requests) {
|
|
755
|
+
const requestKey = `${conversationId}:${requestId}`;
|
|
756
|
+
if (this.primitiveRequestDedupe.has(requestKey))
|
|
757
|
+
continue;
|
|
758
|
+
this.primitiveRequestDedupe.set(requestKey, Date.now());
|
|
759
|
+
let cleared = false;
|
|
760
|
+
try {
|
|
761
|
+
const primitive = value.id;
|
|
762
|
+
if (!isRuntimePrimitiveId(primitive)) {
|
|
763
|
+
cleared = await this.clearRuntimePrimitiveRequest(conversationId, requestId);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const handler = this.primitiveHandlers.get(primitive) ?? this.primitiveFallbackHandler;
|
|
767
|
+
if (!handler) {
|
|
768
|
+
cleared = await this.clearRuntimePrimitiveRequest(conversationId, requestId);
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const args = normalizePrimitiveArgs(value.args);
|
|
772
|
+
await Promise.resolve(handler({
|
|
773
|
+
conversationId,
|
|
774
|
+
primitive,
|
|
775
|
+
args,
|
|
776
|
+
requestId,
|
|
777
|
+
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : undefined,
|
|
778
|
+
rawText: typeof value.rawText === 'string' ? value.rawText : undefined,
|
|
779
|
+
alias: typeof value.alias === 'string' ? value.alias : undefined,
|
|
780
|
+
})).catch((error) => {
|
|
781
|
+
console.error(`[canon-sdk] Runtime primitive ${primitive} handler failed for ${conversationId}:`, error);
|
|
782
|
+
});
|
|
783
|
+
cleared = await this.clearRuntimePrimitiveRequest(conversationId, requestId);
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
if (cleared) {
|
|
787
|
+
this.primitiveRequestDedupe.delete(requestKey);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async clearRuntimePrimitiveRequest(conversationId, requestId) {
|
|
793
|
+
if (!this.agentId)
|
|
794
|
+
return false;
|
|
795
|
+
try {
|
|
796
|
+
await Promise.resolve(rtdbWrite(`/control/${conversationId}/${this.agentId}/primitive/${requestId}`, null));
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
prunePrimitiveRequestDedupe(now = Date.now()) {
|
|
804
|
+
for (const [key, timestamp] of this.primitiveRequestDedupe) {
|
|
805
|
+
if (now - timestamp >= RUNTIME_PRIMITIVE_DEDUPE_TTL_MS) {
|
|
806
|
+
this.primitiveRequestDedupe.delete(key);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
while (this.primitiveRequestDedupe.size > RUNTIME_PRIMITIVE_DEDUPE_MAX) {
|
|
810
|
+
const oldestKey = this.primitiveRequestDedupe.keys().next().value;
|
|
811
|
+
if (!oldestKey)
|
|
812
|
+
break;
|
|
813
|
+
this.primitiveRequestDedupe.delete(oldestKey);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
519
816
|
async handleRuntimeSignal(conversationId, raw) {
|
|
520
817
|
if (!this.agentId)
|
|
521
818
|
return;
|
|
@@ -594,6 +891,13 @@ export class CanonAgent {
|
|
|
594
891
|
hostMode: false,
|
|
595
892
|
});
|
|
596
893
|
}
|
|
894
|
+
requireRuntimeStatePublisher() {
|
|
895
|
+
const publisher = this.createRuntimeStatePublisher();
|
|
896
|
+
if (!publisher) {
|
|
897
|
+
throw new Error('Canon agent must be started before publishing runtime operations.');
|
|
898
|
+
}
|
|
899
|
+
return publisher;
|
|
900
|
+
}
|
|
597
901
|
async handleMessages(conversationId, messages) {
|
|
598
902
|
if (!this.handler) {
|
|
599
903
|
console.warn(`[canon-sdk] No message handler registered — messages for ${conversationId} dropped. Call agent.on('message', handler) before starting.`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { CanonAgent } from './canon-agent.js';
|
|
2
2
|
export type { AgentContactsAPI, AgentUsersAPI } from './canon-agent.js';
|
|
3
3
|
export { CanonApiError, HOST_ADMISSION_ACTION_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, } from '@canonmsg/core';
|
|
4
|
-
export type { CanonContact, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } from '@canonmsg/core';
|
|
4
|
+
export type { CanonContact, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeFact, CanonRuntimeFactGroup, CanonResolveAdmissionResult, ContactAddedPayload, ContactCardPayload, ContactRemovedPayload, ContactSource, HostAdmissionActionCapabilities, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, } from '@canonmsg/core';
|
|
5
5
|
export { SessionManager } from './session-manager.js';
|
|
6
6
|
export { DEFAULT_MEDIA_CACHE_DIR, getCodexImagePath, getMessageAttachments, inferUploadMimeType, isAnthropicImageAttachment, materializeAttachment, materializeMessageMedia, resolveAttachmentMimeType, sendMediaFileMessage, toAnthropicImageBlock, uploadMediaFile, } from './media.js';
|
|
7
7
|
export type { AnthropicImageBlock, AnthropicImageMimeType, MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions, } from './media.js';
|
|
8
8
|
export type { SessionConfig, Session } from './session-manager.js';
|
|
9
9
|
export type { AgentContext, CanonGroupContext, CanonKnownRecentParticipant, CanonMembershipChange, CanonContactRequest, CanonMessage, CanonConversation, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, CreateConversationOptions, } from '@canonmsg/core';
|
|
10
|
-
export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
|
10
|
+
export type { CanonAgentOptions, ContactAddedHandler, ContactRemovedHandler, ContactRequestHandler, MessageHandler, MessageHandlerContext, ProgressMessageOptions, ProgressMessageResult, ReachOutOptions, ReachOutResult, RuntimePrimitiveContext, RuntimePrimitiveHandler, RuntimePrimitiveHandlers, SessionInfo, SessionOptions, DeliveryMode, } from './types.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeDescriptor, CanonMessage, CanonConversation, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
|
|
2
|
-
import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, ContactCardPayload, CanonRuntimeActionDispatch, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
|
|
1
|
+
export type { AddMemberResult, AgentClientType, CanonGroupContext, CanonRuntimeActivityItem, CanonRuntimeActivityKind, CanonRuntimeActivityStatus, CanonRuntimeDescriptor, CanonRuntimeFact, CanonRuntimeFactGroup, CanonRuntimePrimitiveId, CanonMessage, CanonConversation, CanonContact, CanonContactRequest, CanonResolveAdmissionResult, ContactAddedPayload, ContactRemovedPayload, ContactSource, AgentContext, ResolvedAdmissionState, ResolvedAdmissionTargetSummary, ResolvedTargetAdmissionPayload, CanonSelfContext, SendContextualMessageOptions, SendContextualMessageResult, SendContextualSelfContextInput, SendMessageOptions, SessionConfig, CreateConversationOptions, TurnLifecycleState, } from '@canonmsg/core';
|
|
2
|
+
import type { AddMemberResult, CanonGroupContext, CanonMessage, CanonConversation, ContactCardPayload, CanonRuntimeActionDispatch, CanonRuntimePrimitiveId, SendMessageOptions, SendContextualSelfContextInput, SessionConfig } from '@canonmsg/core';
|
|
3
3
|
import type { MaterializeMediaOptions, MaterializedCanonAttachment, ReplyWithFileOptions, UploadMediaFileOptions } from './media.js';
|
|
4
4
|
export interface ProgressMessageOptions extends SendMessageOptions {
|
|
5
5
|
/**
|
|
@@ -123,6 +123,19 @@ export interface RuntimeControlHandlers {
|
|
|
123
123
|
onStopAndDrop?: RuntimeSignalHandler;
|
|
124
124
|
onNewSession?: RuntimeSignalHandler;
|
|
125
125
|
}
|
|
126
|
+
export interface RuntimePrimitiveContext {
|
|
127
|
+
conversationId: string;
|
|
128
|
+
primitive: CanonRuntimePrimitiveId;
|
|
129
|
+
args: Record<string, string | boolean>;
|
|
130
|
+
rawText?: string;
|
|
131
|
+
alias?: string;
|
|
132
|
+
requestId?: string;
|
|
133
|
+
updatedAt?: number;
|
|
134
|
+
}
|
|
135
|
+
export type RuntimePrimitiveHandler = (context: RuntimePrimitiveContext) => void | Promise<void>;
|
|
136
|
+
export type RuntimePrimitiveHandlers = Partial<Record<CanonRuntimePrimitiveId, RuntimePrimitiveHandler>> & {
|
|
137
|
+
'*'?: RuntimePrimitiveHandler;
|
|
138
|
+
};
|
|
126
139
|
export interface CanonAgentOptions {
|
|
127
140
|
apiKey: string;
|
|
128
141
|
baseUrl?: string;
|
|
@@ -141,6 +154,8 @@ export interface CanonAgentOptions {
|
|
|
141
154
|
runtimeDescriptor?: import('@canonmsg/core').CanonRuntimeDescriptor;
|
|
142
155
|
/** Optional Canon runtime signal handlers. Enables interrupt controls when provided. */
|
|
143
156
|
runtimeControls?: RuntimeControlHandlers;
|
|
157
|
+
/** Optional typed runtime primitive handlers. Enables descriptor-backed command controls when provided. */
|
|
158
|
+
runtimePrimitives?: RuntimePrimitiveHandlers;
|
|
144
159
|
/**
|
|
145
160
|
* Enable RTDB session-state reporting. Off by default.
|
|
146
161
|
* Turn-state reporting is automatic while handlers run.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/agent-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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": "^0.
|
|
31
|
+
"@canonmsg/core": "^0.18.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|