@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.
@@ -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
  *
@@ -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 name = process.env.CANON_AGENT;
40
- const p = profiles[name];
41
- if (!p) {
42
- throw new Error(`${prefix} Profile "${name}" not found in ~/.canon/agents.json`);
43
- }
44
- acquireLock(name);
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 name = names[0];
54
- acquireLock(name);
55
- activeResolvedProfile = name;
56
- return { apiKey: profiles[name].apiKey, agentId: profiles[name].agentId, profile: name };
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
- acquireLock(name);
62
- activeResolvedProfile = name;
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 { apiKey: profiles[name].apiKey, agentId: profiles[name].agentId, profile: name };
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';
@@ -0,0 +1,2 @@
1
+ export { AGENT_CAPABILITIES, } from './types.js';
2
+ export { DEFAULT_RUNTIME_CAPABILITIES, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
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';
@@ -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 {};
@@ -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.2.2",
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
- "prepublishOnly": "npm run build"
24
+ "prepack": "npm run build"
21
25
  },
22
26
  "engines": {
23
27
  "node": ">=18.0.0"