@canonmsg/core 0.2.2 → 0.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/dist/agent-resolver.d.ts +4 -0
- package/dist/agent-resolver.js +38 -15
- package/dist/browser.d.ts +4 -0
- package/dist/browser.js +2 -0
- package/dist/client.d.ts +5 -2
- package/dist/client.js +11 -2
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -2
- package/dist/policy.d.ts +99 -0
- package/dist/policy.js +1 -0
- package/dist/rtdb-rest.d.ts +22 -0
- package/dist/rtdb-rest.js +40 -0
- package/dist/turn-protocol.d.ts +55 -0
- package/dist/turn-protocol.js +151 -0
- package/dist/types.d.ts +21 -8
- package/dist/types.js +7 -5
- package/package.json +7 -3
package/dist/agent-resolver.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface ResolvedAgent {
|
|
|
17
17
|
}
|
|
18
18
|
/** Get the currently locked profile name (for cleanup on shutdown). */
|
|
19
19
|
export declare function getActiveProfile(): string | null;
|
|
20
|
+
export declare function resolveCanonProfile(name: string, opts?: {
|
|
21
|
+
logPrefix?: string;
|
|
22
|
+
lock?: boolean;
|
|
23
|
+
}): ResolvedAgent;
|
|
20
24
|
/**
|
|
21
25
|
* Resolve Canon agent credentials.
|
|
22
26
|
*
|
package/dist/agent-resolver.js
CHANGED
|
@@ -16,6 +16,26 @@ let activeResolvedProfile = null;
|
|
|
16
16
|
export function getActiveProfile() {
|
|
17
17
|
return activeResolvedProfile;
|
|
18
18
|
}
|
|
19
|
+
export function resolveCanonProfile(name, opts) {
|
|
20
|
+
const prefix = opts?.logPrefix ?? '[canon]';
|
|
21
|
+
const profileName = name.trim();
|
|
22
|
+
if (!profileName) {
|
|
23
|
+
throw new Error(`${prefix} Profile name is required`);
|
|
24
|
+
}
|
|
25
|
+
const profiles = loadProfiles();
|
|
26
|
+
const profile = profiles[profileName];
|
|
27
|
+
if (!profile) {
|
|
28
|
+
throw new Error(`${prefix} Profile "${profileName}" not found in ~/.canon/agents.json`);
|
|
29
|
+
}
|
|
30
|
+
if (opts?.lock) {
|
|
31
|
+
acquireLock(profileName);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
apiKey: profile.apiKey,
|
|
35
|
+
agentId: profile.agentId,
|
|
36
|
+
profile: profileName,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
19
39
|
/**
|
|
20
40
|
* Resolve Canon agent credentials.
|
|
21
41
|
*
|
|
@@ -36,32 +56,35 @@ export function resolveCanonAgent(opts) {
|
|
|
36
56
|
const names = Object.keys(profiles);
|
|
37
57
|
// 2. Named profile via env var
|
|
38
58
|
if (process.env.CANON_AGENT) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
activeResolvedProfile = name;
|
|
46
|
-
return { apiKey: p.apiKey, agentId: p.agentId, profile: name };
|
|
59
|
+
const resolved = resolveCanonProfile(process.env.CANON_AGENT, {
|
|
60
|
+
logPrefix: prefix,
|
|
61
|
+
lock: true,
|
|
62
|
+
});
|
|
63
|
+
activeResolvedProfile = resolved.profile;
|
|
64
|
+
return resolved;
|
|
47
65
|
}
|
|
48
66
|
// 3. Auto-select from profiles
|
|
49
67
|
if (names.length === 0) {
|
|
50
68
|
throw new Error(`${prefix} No agents registered. Run canon-register first.`);
|
|
51
69
|
}
|
|
52
70
|
if (names.length === 1) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
const resolved = resolveCanonProfile(names[0], {
|
|
72
|
+
logPrefix: prefix,
|
|
73
|
+
lock: true,
|
|
74
|
+
});
|
|
75
|
+
activeResolvedProfile = resolved.profile;
|
|
76
|
+
return resolved;
|
|
57
77
|
}
|
|
58
78
|
// Multiple agents — pick first unlocked
|
|
59
79
|
for (const name of names) {
|
|
60
80
|
if (!isProfileLocked(name).locked) {
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
const resolved = resolveCanonProfile(name, {
|
|
82
|
+
logPrefix: prefix,
|
|
83
|
+
lock: true,
|
|
84
|
+
});
|
|
85
|
+
activeResolvedProfile = resolved.profile;
|
|
63
86
|
console.error(`${prefix} Auto-selected agent "${name}" (${profiles[name].agentName})`);
|
|
64
|
-
return
|
|
87
|
+
return resolved;
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
throw new Error(`${prefix} All agents are in use by other sessions.`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
+
export type { AgentCapabilities, AgentClientType, AgentRuntime, MediaAttachment, MediaAttachmentKind, ModelOption, SessionConfig, WorkspaceOption, } from './types.js';
|
|
3
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
4
|
+
export type { DeliveryIntent, InboundDisposition, RuntimeCapabilities, TriggerDecision, TurnLifecycleState, TurnMessageSemantics, TurnMetadata, TurnState, } from './turn-protocol.js';
|
package/dist/browser.js
ADDED
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type CanonMessage, type CanonConversation, type AgentContext, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
|
|
1
|
+
import { type CanonMessage, type CanonConversation, type AgentContext, type MediaAttachment, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
|
|
2
|
+
import type { InboundDisposition } from './turn-protocol.js';
|
|
2
3
|
/**
|
|
3
4
|
* Thin REST client for Canon's agent API.
|
|
4
5
|
* Uses native fetch — no runtime dependencies.
|
|
@@ -22,11 +23,13 @@ export declare class CanonClient {
|
|
|
22
23
|
createConversation(options: CreateConversationOptions): Promise<{
|
|
23
24
|
conversationId: string;
|
|
24
25
|
}>;
|
|
25
|
-
uploadMedia(conversationId: string, data: string, mimeType: string): Promise<{
|
|
26
|
+
uploadMedia(conversationId: string, data: string, mimeType: string, fileName?: string): Promise<{
|
|
26
27
|
url: string;
|
|
28
|
+
attachment: MediaAttachment;
|
|
27
29
|
}>;
|
|
28
30
|
updateTopic(conversationId: string, topic: string): Promise<void>;
|
|
29
31
|
deleteMessage(conversationId: string, messageId: string): Promise<void>;
|
|
32
|
+
updateMessageDisposition(conversationId: string, messageId: string, inboundDisposition: InboundDisposition): Promise<void>;
|
|
30
33
|
markAsRead(conversationId: string): Promise<void>;
|
|
31
34
|
leaveConversation(conversationId: string): Promise<void>;
|
|
32
35
|
react(conversationId: string, messageId: string, emoji: string): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -73,11 +73,11 @@ export class CanonClient {
|
|
|
73
73
|
throw new CanonApiError(res.status, await res.text());
|
|
74
74
|
return res.json();
|
|
75
75
|
}
|
|
76
|
-
async uploadMedia(conversationId, data, mimeType) {
|
|
76
|
+
async uploadMedia(conversationId, data, mimeType, fileName) {
|
|
77
77
|
const res = await fetch(`${this.baseUrl}/media/upload`, {
|
|
78
78
|
method: 'POST',
|
|
79
79
|
headers: this.authHeaders(),
|
|
80
|
-
body: JSON.stringify({ conversationId, mimeType, data }),
|
|
80
|
+
body: JSON.stringify({ conversationId, mimeType, data, ...(fileName ? { fileName } : {}) }),
|
|
81
81
|
});
|
|
82
82
|
if (!res.ok)
|
|
83
83
|
throw new CanonApiError(res.status, await res.text());
|
|
@@ -100,6 +100,15 @@ export class CanonClient {
|
|
|
100
100
|
if (!res.ok)
|
|
101
101
|
throw new CanonApiError(res.status, await res.text());
|
|
102
102
|
}
|
|
103
|
+
async updateMessageDisposition(conversationId, messageId, inboundDisposition) {
|
|
104
|
+
const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages/${messageId}/disposition`, {
|
|
105
|
+
method: 'PATCH',
|
|
106
|
+
headers: this.authHeaders(),
|
|
107
|
+
body: JSON.stringify({ inboundDisposition }),
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok)
|
|
110
|
+
throw new CanonApiError(res.status, await res.text());
|
|
111
|
+
}
|
|
103
112
|
async markAsRead(conversationId) {
|
|
104
113
|
const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/read`, {
|
|
105
114
|
method: 'POST',
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
-
export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, AgentContext, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
|
|
2
|
+
export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, AgentContext, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
|
|
3
3
|
export { CanonClient, CanonApiError } from './client.js';
|
|
4
4
|
export { CanonStream } from './stream.js';
|
|
5
5
|
export type { StreamHandler } from './stream.js';
|
|
6
|
+
export type { PolicyRole, ParticipationStyle, RepresentationMode, PermissionLevel, ConversationScope, Participant, Relationship, ContextOverlay, BehaviorProfile, AdmissionPolicy, RuntimeControlPolicy, ActionApprovalPolicy, ParticipationPolicy, WorkSession, ResolvedPolicy, ResolvedTurnEligibility, } from './policy.js';
|
|
7
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
8
|
+
export type { DeliveryIntent, TurnMessageSemantics, InboundDisposition, TurnLifecycleState, RuntimeCapabilities, TurnState, TurnMetadata, TriggerDecision, } from './turn-protocol.js';
|
|
6
9
|
export { registerAndWaitForApproval } from './registration.js';
|
|
7
10
|
export { ApprovalManager } from './approval-manager.js';
|
|
8
11
|
export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
|
|
@@ -12,8 +15,8 @@ export { createStreamingHelper } from './streaming.js';
|
|
|
12
15
|
export type { RTDBHandle, RTDBRef, ServerTimestamp, StreamingHelperOptions, StreamingNode } from './streaming.js';
|
|
13
16
|
export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
|
|
14
17
|
export type { AgentProfile } from './agent-profiles.js';
|
|
15
|
-
export { resolveCanonAgent, getActiveProfile } from './agent-resolver.js';
|
|
18
|
+
export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
|
|
16
19
|
export type { ResolvedAgent } from './agent-resolver.js';
|
|
17
|
-
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState } from './rtdb-rest.js';
|
|
18
|
-
export type { SessionStatePayload } from './rtdb-rest.js';
|
|
20
|
+
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
21
|
+
export type { SessionStatePayload, TurnStatePayload } from './rtdb-rest.js';
|
|
19
22
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export { AGENT_CAPABILITIES, } from './types.js';
|
|
|
4
4
|
export { CanonClient, CanonApiError } from './client.js';
|
|
5
5
|
// Stream
|
|
6
6
|
export { CanonStream } from './stream.js';
|
|
7
|
+
// Turn protocol
|
|
8
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
7
9
|
// Registration
|
|
8
10
|
export { registerAndWaitForApproval } from './registration.js';
|
|
9
11
|
// Approval
|
|
@@ -15,8 +17,8 @@ export { createStreamingHelper } from './streaming.js';
|
|
|
15
17
|
// Agent profiles (loading, locking, resolution)
|
|
16
18
|
export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
|
|
17
19
|
// Agent resolver
|
|
18
|
-
export { resolveCanonAgent, getActiveProfile } from './agent-resolver.js';
|
|
20
|
+
export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
|
|
19
21
|
// RTDB REST helpers (token exchange, session state, generic read/write)
|
|
20
|
-
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState } from './rtdb-rest.js';
|
|
22
|
+
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
21
23
|
// Constants
|
|
22
24
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export type PolicyRole = 'owner' | 'conversation_member' | 'group_admin' | 'external_requester' | 'agent_self';
|
|
2
|
+
export type ParticipationStyle = 'natural' | 'collaborative' | 'mention-first' | 'approval-gated' | 'handoff-only' | 'observer';
|
|
3
|
+
export type RepresentationMode = 'self' | 'delegate';
|
|
4
|
+
export type PermissionLevel = 'deny' | 'allow' | 'require_approval';
|
|
5
|
+
export type ConversationScope = 'global' | 'relationship' | 'conversation' | 'work_session' | 'message';
|
|
6
|
+
export interface Participant {
|
|
7
|
+
id: string;
|
|
8
|
+
type: 'human' | 'ai_agent';
|
|
9
|
+
ownerId?: string | null;
|
|
10
|
+
relationshipIds?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface Relationship {
|
|
13
|
+
id: string;
|
|
14
|
+
participantIds: [string, string];
|
|
15
|
+
profileId?: string | null;
|
|
16
|
+
notes?: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface ContextOverlay {
|
|
19
|
+
scope: ConversationScope;
|
|
20
|
+
scopeId: string;
|
|
21
|
+
instructions?: string | null;
|
|
22
|
+
notes?: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface BehaviorProfile {
|
|
25
|
+
id: string;
|
|
26
|
+
label: string;
|
|
27
|
+
participationStyle: ParticipationStyle;
|
|
28
|
+
defaultRepresentation: RepresentationMode;
|
|
29
|
+
allowAgentToAgent: boolean;
|
|
30
|
+
allowLongRunningCollaboration: boolean;
|
|
31
|
+
requireMentionForGroupAgentReplies: boolean;
|
|
32
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
33
|
+
}
|
|
34
|
+
export interface AdmissionPolicy {
|
|
35
|
+
canDiscover: PermissionLevel;
|
|
36
|
+
canStartDirectConversation: PermissionLevel;
|
|
37
|
+
canAddToGroup: PermissionLevel;
|
|
38
|
+
canSendContactRequest: PermissionLevel;
|
|
39
|
+
}
|
|
40
|
+
export interface RuntimeControlPolicy {
|
|
41
|
+
canInterrupt: PermissionLevel;
|
|
42
|
+
canChangeModel: PermissionLevel;
|
|
43
|
+
canChangeWorkspace: PermissionLevel;
|
|
44
|
+
canChangeSessionConfig: PermissionLevel;
|
|
45
|
+
}
|
|
46
|
+
export interface ActionApprovalPolicy {
|
|
47
|
+
canInitiateExternalFirstContact: PermissionLevel;
|
|
48
|
+
canShareIdentity: PermissionLevel;
|
|
49
|
+
canIntroduceParticipant: PermissionLevel;
|
|
50
|
+
canSpeakAsDelegate: PermissionLevel;
|
|
51
|
+
}
|
|
52
|
+
export interface ParticipationPolicy {
|
|
53
|
+
style: ParticipationStyle;
|
|
54
|
+
allowAgentToAgent: boolean;
|
|
55
|
+
allowHumanToAgent: boolean;
|
|
56
|
+
allowLongRunningCollaboration: boolean;
|
|
57
|
+
requireMentionForGroupAgentReplies: boolean;
|
|
58
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
59
|
+
}
|
|
60
|
+
export interface WorkSession {
|
|
61
|
+
id: string;
|
|
62
|
+
conversationId?: string | null;
|
|
63
|
+
label?: string | null;
|
|
64
|
+
objective?: string | null;
|
|
65
|
+
participationStyle?: ParticipationStyle;
|
|
66
|
+
activeParticipantIds: string[];
|
|
67
|
+
overlayIds?: string[];
|
|
68
|
+
status: 'active' | 'paused' | 'completed';
|
|
69
|
+
}
|
|
70
|
+
export interface ResolvedPolicy {
|
|
71
|
+
admission: AdmissionPolicy;
|
|
72
|
+
runtime: RuntimeControlPolicy;
|
|
73
|
+
actionApproval: ActionApprovalPolicy;
|
|
74
|
+
participation: ParticipationPolicy;
|
|
75
|
+
representation: {
|
|
76
|
+
defaultMode: RepresentationMode;
|
|
77
|
+
};
|
|
78
|
+
overlays: ContextOverlay[];
|
|
79
|
+
resolvedFrom: {
|
|
80
|
+
globalDefault?: string;
|
|
81
|
+
agentDefault?: string;
|
|
82
|
+
relationship?: string;
|
|
83
|
+
workSession?: string;
|
|
84
|
+
messageDirective?: string;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export interface ResolvedTurnEligibility {
|
|
88
|
+
mayAutoReply: boolean;
|
|
89
|
+
mayInterrupt: boolean;
|
|
90
|
+
mayQueueWhileRunning: boolean;
|
|
91
|
+
mayInterruptRunningTurn: boolean;
|
|
92
|
+
mayInterleaveWhileRunning: boolean;
|
|
93
|
+
mayReactToNonFinalAgentMessages: boolean;
|
|
94
|
+
mayChangeModel: boolean;
|
|
95
|
+
mayChangeWorkspace: boolean;
|
|
96
|
+
maySpeakAsDelegate: boolean;
|
|
97
|
+
mayInitiateExternalFirstContact: boolean;
|
|
98
|
+
reasonCodes: string[];
|
|
99
|
+
}
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/rtdb-rest.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* is exchanged for a Firebase ID token before use with RTDB REST.
|
|
7
7
|
*/
|
|
8
8
|
import type { CanonClient } from './client.js';
|
|
9
|
+
import type { DeliveryIntent, RuntimeCapabilities, TurnLifecycleState } from './turn-protocol.js';
|
|
9
10
|
export interface SessionStatePayload {
|
|
11
|
+
lastError?: string;
|
|
10
12
|
model?: string;
|
|
11
13
|
permissionMode?: string;
|
|
12
14
|
effort?: string;
|
|
@@ -28,6 +30,24 @@ export interface SessionStatePayload {
|
|
|
28
30
|
'.sv': 'timestamp';
|
|
29
31
|
};
|
|
30
32
|
}
|
|
33
|
+
export interface TurnStatePayload {
|
|
34
|
+
turnId?: string | null;
|
|
35
|
+
state: TurnLifecycleState;
|
|
36
|
+
queueDepth: number;
|
|
37
|
+
currentSpeakerId?: string | null;
|
|
38
|
+
lastAcceptedIntent?: DeliveryIntent | null;
|
|
39
|
+
activeMessageIds?: string[];
|
|
40
|
+
capabilities?: RuntimeCapabilities;
|
|
41
|
+
openedAt?: number | {
|
|
42
|
+
'.sv': 'timestamp';
|
|
43
|
+
};
|
|
44
|
+
completedAt?: number | {
|
|
45
|
+
'.sv': 'timestamp';
|
|
46
|
+
} | null;
|
|
47
|
+
updatedAt: {
|
|
48
|
+
'.sv': 'timestamp';
|
|
49
|
+
};
|
|
50
|
+
}
|
|
31
51
|
/** Must be called once before any RTDB operations. */
|
|
32
52
|
export declare function initRTDBAuth(client: CanonClient): void;
|
|
33
53
|
/** Generic RTDB REST write (PUT). */
|
|
@@ -43,3 +63,5 @@ export declare function writeSessionState(conversationId: string, agentId: strin
|
|
|
43
63
|
* Clear session state from RTDB (full overwrite with isActive: false).
|
|
44
64
|
*/
|
|
45
65
|
export declare function clearSessionState(conversationId: string, agentId: string): Promise<void>;
|
|
66
|
+
export declare function writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
|
|
67
|
+
export declare function clearTurnState(conversationId: string, agentId: string): Promise<void>;
|
package/dist/rtdb-rest.js
CHANGED
|
@@ -126,3 +126,43 @@ export async function clearSessionState(conversationId, agentId) {
|
|
|
126
126
|
console.error(`[canon] RTDB clear failed (${res.status}): ${text}`);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
export async function writeTurnState(conversationId, agentId, state) {
|
|
130
|
+
const token = await getToken();
|
|
131
|
+
if (!token)
|
|
132
|
+
return;
|
|
133
|
+
const url = `${RTDB_BASE}/turn-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
134
|
+
const body = {
|
|
135
|
+
...state,
|
|
136
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
137
|
+
};
|
|
138
|
+
const res = await fetch(url, {
|
|
139
|
+
method: 'PUT',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: JSON.stringify(body),
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
const text = await res.text();
|
|
145
|
+
throw new Error(`RTDB write failed (${res.status}): ${text}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function clearTurnState(conversationId, agentId) {
|
|
149
|
+
const token = await getToken();
|
|
150
|
+
if (!token)
|
|
151
|
+
return;
|
|
152
|
+
const url = `${RTDB_BASE}/turn-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
153
|
+
const body = {
|
|
154
|
+
state: 'idle',
|
|
155
|
+
queueDepth: 0,
|
|
156
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
157
|
+
completedAt: { '.sv': 'timestamp' },
|
|
158
|
+
};
|
|
159
|
+
const res = await fetch(url, {
|
|
160
|
+
method: 'PUT',
|
|
161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify(body),
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const text = await res.text();
|
|
166
|
+
console.error(`[canon] RTDB turn clear failed (${res.status}): ${text}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type DeliveryIntent = 'queue' | 'interrupt' | 'interleave' | 'stop';
|
|
2
|
+
export type TurnMessageSemantics = 'progress' | 'turn_complete' | 'control';
|
|
3
|
+
export type InboundDisposition = 'queued' | 'accepted_now' | 'interleaved' | 'trigger_suppressed' | 'rejected';
|
|
4
|
+
export type TurnLifecycleState = 'idle' | 'thinking' | 'streaming' | 'tool' | 'waiting_input' | 'completed' | 'interrupted';
|
|
5
|
+
export interface RuntimeCapabilities {
|
|
6
|
+
supportsInterrupt: boolean;
|
|
7
|
+
supportsQueue: boolean;
|
|
8
|
+
supportsInterleave: boolean;
|
|
9
|
+
supportsRequiresAction: boolean;
|
|
10
|
+
supportsNonFinalPermanentMessages: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface TurnState {
|
|
13
|
+
turnId?: string | null;
|
|
14
|
+
state: TurnLifecycleState;
|
|
15
|
+
queueDepth: number;
|
|
16
|
+
currentSpeakerId?: string | null;
|
|
17
|
+
lastAcceptedIntent?: DeliveryIntent | null;
|
|
18
|
+
activeMessageIds?: string[];
|
|
19
|
+
capabilities?: RuntimeCapabilities;
|
|
20
|
+
openedAt?: number;
|
|
21
|
+
updatedAt?: number;
|
|
22
|
+
completedAt?: number | null;
|
|
23
|
+
}
|
|
24
|
+
export interface TurnMetadata {
|
|
25
|
+
turnId?: string | null;
|
|
26
|
+
turnSemantics?: TurnMessageSemantics;
|
|
27
|
+
deliveryIntent?: DeliveryIntent;
|
|
28
|
+
turnComplete?: boolean;
|
|
29
|
+
replyBehavior?: 'allow_auto_reply' | 'suppress_auto_reply';
|
|
30
|
+
inboundDisposition?: InboundDisposition;
|
|
31
|
+
}
|
|
32
|
+
export interface TriggerDecision {
|
|
33
|
+
allow: boolean;
|
|
34
|
+
semantics: TurnMessageSemantics;
|
|
35
|
+
reason: string;
|
|
36
|
+
}
|
|
37
|
+
export declare const DEFAULT_RUNTIME_CAPABILITIES: RuntimeCapabilities;
|
|
38
|
+
export declare function normalizeTurnMetadata(metadata: unknown): TurnMetadata | null;
|
|
39
|
+
export declare function normalizeTurnState(value: unknown): TurnState | null;
|
|
40
|
+
export declare function isTurnOpen(turnState: Pick<TurnState, 'state'> | null | undefined): boolean;
|
|
41
|
+
export declare function resolveTurnMessageSemantics(input: {
|
|
42
|
+
senderType: 'human' | 'ai_agent';
|
|
43
|
+
metadata?: unknown;
|
|
44
|
+
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
45
|
+
}): TurnMessageSemantics;
|
|
46
|
+
export declare function shouldPromoteConversationMessage(input: {
|
|
47
|
+
senderType: 'human' | 'ai_agent';
|
|
48
|
+
metadata?: unknown;
|
|
49
|
+
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
50
|
+
}): boolean;
|
|
51
|
+
export declare function shouldTriggerAgentTurn(input: {
|
|
52
|
+
senderType: 'human' | 'ai_agent';
|
|
53
|
+
metadata?: unknown;
|
|
54
|
+
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
55
|
+
}): TriggerDecision;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const TURN_STATES = [
|
|
2
|
+
'idle',
|
|
3
|
+
'thinking',
|
|
4
|
+
'streaming',
|
|
5
|
+
'tool',
|
|
6
|
+
'waiting_input',
|
|
7
|
+
'completed',
|
|
8
|
+
'interrupted',
|
|
9
|
+
];
|
|
10
|
+
const TURN_SEMANTICS = [
|
|
11
|
+
'progress',
|
|
12
|
+
'turn_complete',
|
|
13
|
+
'control',
|
|
14
|
+
];
|
|
15
|
+
const DELIVERY_INTENTS = [
|
|
16
|
+
'queue',
|
|
17
|
+
'interrupt',
|
|
18
|
+
'interleave',
|
|
19
|
+
'stop',
|
|
20
|
+
];
|
|
21
|
+
export const DEFAULT_RUNTIME_CAPABILITIES = {
|
|
22
|
+
supportsInterrupt: false,
|
|
23
|
+
supportsQueue: true,
|
|
24
|
+
supportsInterleave: false,
|
|
25
|
+
supportsRequiresAction: false,
|
|
26
|
+
supportsNonFinalPermanentMessages: false,
|
|
27
|
+
};
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
export function normalizeTurnMetadata(metadata) {
|
|
32
|
+
if (!isRecord(metadata))
|
|
33
|
+
return null;
|
|
34
|
+
const turnId = typeof metadata.turnId === 'string' && metadata.turnId.trim()
|
|
35
|
+
? metadata.turnId.trim()
|
|
36
|
+
: null;
|
|
37
|
+
const turnSemantics = TURN_SEMANTICS.includes(metadata.turnSemantics)
|
|
38
|
+
? metadata.turnSemantics
|
|
39
|
+
: undefined;
|
|
40
|
+
const deliveryIntent = DELIVERY_INTENTS.includes(metadata.deliveryIntent)
|
|
41
|
+
? metadata.deliveryIntent
|
|
42
|
+
: undefined;
|
|
43
|
+
const replyBehavior = metadata.replyBehavior === 'allow_auto_reply' || metadata.replyBehavior === 'suppress_auto_reply'
|
|
44
|
+
? metadata.replyBehavior
|
|
45
|
+
: undefined;
|
|
46
|
+
const inboundDisposition = metadata.inboundDisposition === 'queued'
|
|
47
|
+
|| metadata.inboundDisposition === 'accepted_now'
|
|
48
|
+
|| metadata.inboundDisposition === 'interleaved'
|
|
49
|
+
|| metadata.inboundDisposition === 'trigger_suppressed'
|
|
50
|
+
|| metadata.inboundDisposition === 'rejected'
|
|
51
|
+
? metadata.inboundDisposition
|
|
52
|
+
: undefined;
|
|
53
|
+
if (!turnId && !turnSemantics && !deliveryIntent && typeof metadata.turnComplete !== 'boolean'
|
|
54
|
+
&& !replyBehavior && !inboundDisposition) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...(turnId ? { turnId } : {}),
|
|
59
|
+
...(turnSemantics ? { turnSemantics } : {}),
|
|
60
|
+
...(deliveryIntent ? { deliveryIntent } : {}),
|
|
61
|
+
...(typeof metadata.turnComplete === 'boolean' ? { turnComplete: metadata.turnComplete } : {}),
|
|
62
|
+
...(replyBehavior ? { replyBehavior } : {}),
|
|
63
|
+
...(inboundDisposition ? { inboundDisposition } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function normalizeTurnState(value) {
|
|
67
|
+
if (!isRecord(value))
|
|
68
|
+
return null;
|
|
69
|
+
const state = TURN_STATES.includes(value.state)
|
|
70
|
+
? value.state
|
|
71
|
+
: null;
|
|
72
|
+
if (!state)
|
|
73
|
+
return null;
|
|
74
|
+
return {
|
|
75
|
+
state,
|
|
76
|
+
queueDepth: typeof value.queueDepth === 'number' ? value.queueDepth : 0,
|
|
77
|
+
turnId: typeof value.turnId === 'string' ? value.turnId : null,
|
|
78
|
+
currentSpeakerId: typeof value.currentSpeakerId === 'string' ? value.currentSpeakerId : null,
|
|
79
|
+
lastAcceptedIntent: DELIVERY_INTENTS.includes(value.lastAcceptedIntent)
|
|
80
|
+
? value.lastAcceptedIntent
|
|
81
|
+
: null,
|
|
82
|
+
activeMessageIds: Array.isArray(value.activeMessageIds)
|
|
83
|
+
? value.activeMessageIds.filter((entry) => typeof entry === 'string')
|
|
84
|
+
: [],
|
|
85
|
+
capabilities: isRecord(value.capabilities)
|
|
86
|
+
? {
|
|
87
|
+
supportsInterrupt: Boolean(value.capabilities.supportsInterrupt),
|
|
88
|
+
supportsQueue: value.capabilities.supportsQueue !== false,
|
|
89
|
+
supportsInterleave: Boolean(value.capabilities.supportsInterleave),
|
|
90
|
+
supportsRequiresAction: Boolean(value.capabilities.supportsRequiresAction),
|
|
91
|
+
supportsNonFinalPermanentMessages: Boolean(value.capabilities.supportsNonFinalPermanentMessages),
|
|
92
|
+
}
|
|
93
|
+
: undefined,
|
|
94
|
+
openedAt: typeof value.openedAt === 'number' ? value.openedAt : undefined,
|
|
95
|
+
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : undefined,
|
|
96
|
+
completedAt: typeof value.completedAt === 'number' ? value.completedAt : null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function isTurnOpen(turnState) {
|
|
100
|
+
if (!turnState)
|
|
101
|
+
return false;
|
|
102
|
+
return turnState.state !== 'idle'
|
|
103
|
+
&& turnState.state !== 'completed'
|
|
104
|
+
&& turnState.state !== 'interrupted';
|
|
105
|
+
}
|
|
106
|
+
export function resolveTurnMessageSemantics(input) {
|
|
107
|
+
const turnMetadata = normalizeTurnMetadata(input.metadata);
|
|
108
|
+
if (turnMetadata?.turnSemantics) {
|
|
109
|
+
return turnMetadata.turnSemantics;
|
|
110
|
+
}
|
|
111
|
+
if (turnMetadata?.turnComplete === true) {
|
|
112
|
+
return 'turn_complete';
|
|
113
|
+
}
|
|
114
|
+
if (input.senderType === 'human') {
|
|
115
|
+
return 'turn_complete';
|
|
116
|
+
}
|
|
117
|
+
return isTurnOpen(input.senderTurnState) ? 'progress' : 'turn_complete';
|
|
118
|
+
}
|
|
119
|
+
export function shouldPromoteConversationMessage(input) {
|
|
120
|
+
return resolveTurnMessageSemantics(input) !== 'progress';
|
|
121
|
+
}
|
|
122
|
+
export function shouldTriggerAgentTurn(input) {
|
|
123
|
+
const semantics = resolveTurnMessageSemantics(input);
|
|
124
|
+
const turnMetadata = normalizeTurnMetadata(input.metadata);
|
|
125
|
+
if (turnMetadata?.replyBehavior === 'suppress_auto_reply') {
|
|
126
|
+
return {
|
|
127
|
+
allow: false,
|
|
128
|
+
semantics,
|
|
129
|
+
reason: 'metadata explicitly suppresses auto-reply',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (input.senderType === 'human') {
|
|
133
|
+
return {
|
|
134
|
+
allow: true,
|
|
135
|
+
semantics,
|
|
136
|
+
reason: 'human messages always remain triggerable',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (semantics === 'progress') {
|
|
140
|
+
return {
|
|
141
|
+
allow: false,
|
|
142
|
+
semantics,
|
|
143
|
+
reason: 'non-final agent progress does not trigger other agents',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
allow: true,
|
|
148
|
+
semantics,
|
|
149
|
+
reason: 'agent message is treated as turn-complete',
|
|
150
|
+
};
|
|
151
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
+
export type MediaAttachmentKind = 'image' | 'audio' | 'file';
|
|
2
|
+
export interface MediaAttachment {
|
|
3
|
+
kind: MediaAttachmentKind;
|
|
4
|
+
url: string;
|
|
5
|
+
mimeType?: string;
|
|
6
|
+
fileName?: string;
|
|
7
|
+
sizeBytes?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
durationMs?: number;
|
|
11
|
+
}
|
|
1
12
|
export interface CanonMessage {
|
|
2
13
|
id: string;
|
|
3
14
|
senderId: string;
|
|
4
15
|
senderType: 'human' | 'ai_agent';
|
|
5
16
|
/** Whether the sender is this agent's owner (server-computed, trusted) */
|
|
6
17
|
isOwner: boolean;
|
|
7
|
-
contentType: 'text' | 'image' | 'audio' | 'contact_card';
|
|
18
|
+
contentType: 'text' | 'image' | 'audio' | 'file' | 'contact_card';
|
|
8
19
|
text: string | null;
|
|
9
20
|
imageUrl: string | null;
|
|
10
21
|
audioUrl: string | null;
|
|
11
22
|
audioDurationMs: number | null;
|
|
23
|
+
attachments?: MediaAttachment[];
|
|
12
24
|
mentions: string[];
|
|
13
25
|
replyTo: string | null;
|
|
14
26
|
replyToPosition: number | null;
|
|
@@ -41,6 +53,8 @@ export interface AgentCapabilities {
|
|
|
41
53
|
supportsEffort: boolean;
|
|
42
54
|
supportsSessionState: boolean;
|
|
43
55
|
supportsInterrupt: boolean;
|
|
56
|
+
supportsQueue?: boolean;
|
|
57
|
+
supportsInterleave?: boolean;
|
|
44
58
|
}
|
|
45
59
|
export interface ModelOption {
|
|
46
60
|
value: string;
|
|
@@ -50,11 +64,7 @@ export interface WorkspaceOption {
|
|
|
50
64
|
id: string;
|
|
51
65
|
label: string;
|
|
52
66
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Capability map keyed by clientType. Add new agent types here.
|
|
55
|
-
* ALSO update the duplicate in app/src/types/index.ts (RN app can't
|
|
56
|
-
* import from core directly due to different bundler/runtime).
|
|
57
|
-
*/
|
|
67
|
+
/** Capability map keyed by clientType. Add new agent types here. */
|
|
58
68
|
export declare const AGENT_CAPABILITIES: Record<AgentClientType, AgentCapabilities>;
|
|
59
69
|
/** Trusted agent identity & access context, provided by the server */
|
|
60
70
|
export interface AgentContext {
|
|
@@ -78,10 +88,11 @@ export interface MessageCreatedPayload {
|
|
|
78
88
|
/** Whether the sender is this agent's owner (server-computed, trusted) */
|
|
79
89
|
isOwner?: boolean;
|
|
80
90
|
text?: string;
|
|
81
|
-
contentType?: 'text' | 'image' | 'audio' | 'contact_card';
|
|
91
|
+
contentType?: 'text' | 'image' | 'audio' | 'file' | 'contact_card';
|
|
82
92
|
imageUrl?: string;
|
|
83
93
|
audioUrl?: string;
|
|
84
94
|
audioDurationMs?: number;
|
|
95
|
+
attachments?: MediaAttachment[];
|
|
85
96
|
replyTo?: string;
|
|
86
97
|
replyToPosition?: number;
|
|
87
98
|
mentions?: string[];
|
|
@@ -101,12 +112,13 @@ export interface PresencePayload {
|
|
|
101
112
|
online: boolean;
|
|
102
113
|
}
|
|
103
114
|
export interface SendMessageOptions {
|
|
104
|
-
contentType?: 'text' | 'audio' | 'image' | 'contact_card';
|
|
115
|
+
contentType?: 'text' | 'audio' | 'image' | 'file' | 'contact_card';
|
|
105
116
|
replyTo?: string;
|
|
106
117
|
replyToPosition?: number;
|
|
107
118
|
audioUrl?: string;
|
|
108
119
|
audioDurationMs?: number;
|
|
109
120
|
imageUrl?: string;
|
|
121
|
+
attachments?: MediaAttachment[];
|
|
110
122
|
contactCardUserId?: string;
|
|
111
123
|
mentions?: string[];
|
|
112
124
|
/** Structured metadata for rich UI (approval cards, etc.) */
|
|
@@ -135,6 +147,7 @@ export interface SessionControl {
|
|
|
135
147
|
}
|
|
136
148
|
/** Written by agent to /session-state/{convoId}/{agentId} in RTDB */
|
|
137
149
|
export interface SessionState {
|
|
150
|
+
lastError?: string;
|
|
138
151
|
model?: string;
|
|
139
152
|
permissionMode?: string;
|
|
140
153
|
effort?: string;
|
package/dist/types.js
CHANGED
|
@@ -5,12 +5,10 @@ const DEFAULT_CAPABILITIES = {
|
|
|
5
5
|
supportsEffort: false,
|
|
6
6
|
supportsSessionState: false,
|
|
7
7
|
supportsInterrupt: false,
|
|
8
|
+
supportsQueue: true,
|
|
9
|
+
supportsInterleave: false,
|
|
8
10
|
};
|
|
9
|
-
/**
|
|
10
|
-
* Capability map keyed by clientType. Add new agent types here.
|
|
11
|
-
* ALSO update the duplicate in app/src/types/index.ts (RN app can't
|
|
12
|
-
* import from core directly due to different bundler/runtime).
|
|
13
|
-
*/
|
|
11
|
+
/** Capability map keyed by clientType. Add new agent types here. */
|
|
14
12
|
export const AGENT_CAPABILITIES = {
|
|
15
13
|
'claude-code': {
|
|
16
14
|
supportsModelSwitch: true,
|
|
@@ -18,6 +16,8 @@ export const AGENT_CAPABILITIES = {
|
|
|
18
16
|
supportsEffort: true,
|
|
19
17
|
supportsSessionState: true,
|
|
20
18
|
supportsInterrupt: true,
|
|
19
|
+
supportsQueue: true,
|
|
20
|
+
supportsInterleave: true,
|
|
21
21
|
},
|
|
22
22
|
'codex': {
|
|
23
23
|
supportsModelSwitch: false,
|
|
@@ -25,6 +25,8 @@ export const AGENT_CAPABILITIES = {
|
|
|
25
25
|
supportsEffort: false,
|
|
26
26
|
supportsSessionState: true,
|
|
27
27
|
supportsInterrupt: true,
|
|
28
|
+
supportsQueue: true,
|
|
29
|
+
supportsInterleave: false,
|
|
28
30
|
},
|
|
29
31
|
'openclaw': { ...DEFAULT_CAPABILITIES },
|
|
30
32
|
'generic': { ...DEFAULT_CAPABILITIES },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./browser": {
|
|
14
|
+
"import": "./dist/browser.js",
|
|
15
|
+
"types": "./dist/browser.d.ts"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
15
19
|
"dist"
|
|
16
20
|
],
|
|
17
21
|
"scripts": {
|
|
18
|
-
"build": "tsc",
|
|
22
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
19
23
|
"dev": "tsc --watch",
|
|
20
|
-
"
|
|
24
|
+
"prepack": "npm run build"
|
|
21
25
|
},
|
|
22
26
|
"engines": {
|
|
23
27
|
"node": ">=18.0.0"
|