@canonmsg/backend-contracts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ import { getMessageAttachments } from './media.js';
2
+ const MISSING_CREATED_AT_ISO = new Date(0).toISOString();
3
+ function isRecord(value) {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ }
6
+ function normalizeStringArray(value) {
7
+ if (!Array.isArray(value))
8
+ return [];
9
+ return value.filter((entry) => typeof entry === 'string');
10
+ }
11
+ function normalizeTimestamp(value) {
12
+ if (value instanceof Date)
13
+ return value;
14
+ if (isRecord(value) && typeof value.toDate === 'function') {
15
+ const result = value.toDate();
16
+ if (result instanceof Date)
17
+ return result;
18
+ }
19
+ return null;
20
+ }
21
+ function normalizeCreatedAt(value, fallback) {
22
+ const timestamp = normalizeTimestamp(value) ?? normalizeTimestamp(fallback);
23
+ return timestamp ? timestamp.toISOString() : MISSING_CREATED_AT_ISO;
24
+ }
25
+ function normalizeSenderType(value, fallback) {
26
+ if (value === 'ai_agent' || fallback === 'ai_agent')
27
+ return 'ai_agent';
28
+ return 'human';
29
+ }
30
+ function normalizeContentType(value, attachments) {
31
+ if (value === 'text'
32
+ || value === 'image'
33
+ || value === 'audio'
34
+ || value === 'file'
35
+ || value === 'contact_card') {
36
+ return value;
37
+ }
38
+ const primary = attachments[0]?.kind;
39
+ if (primary === 'image' || primary === 'audio' || primary === 'file')
40
+ return primary;
41
+ return 'text';
42
+ }
43
+ function normalizeForwardedFrom(value) {
44
+ if (!isRecord(value))
45
+ return undefined;
46
+ if (typeof value.sourceConversationId !== 'string')
47
+ return undefined;
48
+ if (typeof value.messageId !== 'string')
49
+ return undefined;
50
+ return {
51
+ sourceConversationId: value.sourceConversationId,
52
+ messageId: value.messageId,
53
+ };
54
+ }
55
+ function normalizeContactCard(value) {
56
+ if (!isRecord(value))
57
+ return undefined;
58
+ if (typeof value.userId !== 'string' || value.userId.length === 0)
59
+ return undefined;
60
+ const userType = value.userType === 'ai_agent' ? 'ai_agent' : 'human';
61
+ const card = {
62
+ userId: value.userId,
63
+ displayName: typeof value.displayName === 'string' ? value.displayName : 'Unknown',
64
+ avatarUrl: typeof value.avatarUrl === 'string' ? value.avatarUrl : null,
65
+ userType,
66
+ };
67
+ if (typeof value.about === 'string')
68
+ card.about = value.about;
69
+ if (typeof value.isActive === 'boolean')
70
+ card.isActive = value.isActive;
71
+ if (typeof value.ownerId === 'string')
72
+ card.ownerId = value.ownerId;
73
+ if (typeof value.ownerName === 'string')
74
+ card.ownerName = value.ownerName;
75
+ if (typeof value.lifecycleState === 'string')
76
+ card.lifecycleState = value.lifecycleState;
77
+ return card;
78
+ }
79
+ export function serializeStoredMessage(input) {
80
+ const { data } = input;
81
+ const attachments = getMessageAttachments({
82
+ attachments: data.attachments,
83
+ });
84
+ const result = {
85
+ id: input.id,
86
+ senderId: typeof data.senderId === 'string' ? data.senderId : '',
87
+ senderType: normalizeSenderType(data.senderType, input.senderTypeFallback),
88
+ isOwner: input.isOwner,
89
+ contentType: normalizeContentType(data.contentType, attachments),
90
+ text: typeof data.text === 'string' ? data.text : null,
91
+ attachments,
92
+ mentions: normalizeStringArray(data.mentions),
93
+ replyTo: typeof data.replyTo === 'string' ? data.replyTo : null,
94
+ replyToPosition: typeof data.replyToPosition === 'number' ? data.replyToPosition : null,
95
+ status: data.status === 'read' ? 'read' : 'sent',
96
+ createdAt: normalizeCreatedAt(data.createdAt, input.createdAtFallback),
97
+ };
98
+ const forwardedFrom = normalizeForwardedFrom(data.forwardedFrom);
99
+ if (data.forwarded === true || forwardedFrom) {
100
+ result.forwarded = true;
101
+ }
102
+ if (forwardedFrom) {
103
+ result.forwardedFrom = forwardedFrom;
104
+ }
105
+ if (typeof input.senderName === 'string') {
106
+ result.senderName = input.senderName;
107
+ }
108
+ if (data.workSession !== undefined && data.workSession !== null) {
109
+ result.workSession = data.workSession;
110
+ }
111
+ if (isRecord(data.metadata)) {
112
+ result.metadata = data.metadata;
113
+ }
114
+ const contactCard = normalizeContactCard(data.contactCard);
115
+ if (contactCard) {
116
+ result.contactCard = contactCard;
117
+ }
118
+ return result;
119
+ }
@@ -0,0 +1,32 @@
1
+ export type SenderType = 'human' | 'ai_agent';
2
+ export type TurnLifecycleState = 'idle' | 'thinking' | 'streaming' | 'tool' | 'waiting_input' | 'completed' | 'interrupted';
3
+ export type TurnMessageSemantics = 'progress' | 'turn_complete' | 'control';
4
+ export interface TurnMetadata {
5
+ turnSemantics?: TurnMessageSemantics;
6
+ turnComplete?: boolean;
7
+ replyBehavior?: 'allow_auto_reply' | 'suppress_auto_reply';
8
+ }
9
+ export interface TurnStateLike {
10
+ state: TurnLifecycleState;
11
+ }
12
+ export declare function normalizeTurnMetadata(metadata: unknown): TurnMetadata | null;
13
+ export declare function isTurnOpen(turnState: TurnStateLike | null | undefined): boolean;
14
+ export declare function resolveTurnMessageSemantics(input: {
15
+ senderType: SenderType;
16
+ metadata?: unknown;
17
+ senderTurnState?: TurnStateLike | null;
18
+ }): TurnMessageSemantics;
19
+ export declare function shouldPromoteConversationMessage(input: {
20
+ senderType: SenderType;
21
+ metadata?: unknown;
22
+ senderTurnState?: TurnStateLike | null;
23
+ }): boolean;
24
+ export declare function shouldTriggerAgentTurn(input: {
25
+ senderType: SenderType;
26
+ metadata?: unknown;
27
+ senderTurnState?: TurnStateLike | null;
28
+ }): {
29
+ allow: boolean;
30
+ semantics: TurnMessageSemantics;
31
+ };
32
+ export declare function normalizeRuntimeTurnState(value: unknown): TurnStateLike | null;
@@ -0,0 +1,75 @@
1
+ function isRecord(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ export function normalizeTurnMetadata(metadata) {
5
+ if (!isRecord(metadata))
6
+ return null;
7
+ const turnSemantics = metadata.turnSemantics === 'progress'
8
+ || metadata.turnSemantics === 'turn_complete'
9
+ || metadata.turnSemantics === 'control'
10
+ ? metadata.turnSemantics
11
+ : undefined;
12
+ const replyBehavior = metadata.replyBehavior === 'allow_auto_reply'
13
+ || metadata.replyBehavior === 'suppress_auto_reply'
14
+ ? metadata.replyBehavior
15
+ : undefined;
16
+ if (!turnSemantics && typeof metadata.turnComplete !== 'boolean' && !replyBehavior) {
17
+ return null;
18
+ }
19
+ return {
20
+ ...(turnSemantics ? { turnSemantics } : {}),
21
+ ...(typeof metadata.turnComplete === 'boolean' ? { turnComplete: metadata.turnComplete } : {}),
22
+ ...(replyBehavior ? { replyBehavior } : {}),
23
+ };
24
+ }
25
+ export function isTurnOpen(turnState) {
26
+ if (!turnState)
27
+ return false;
28
+ return turnState.state !== 'idle'
29
+ && turnState.state !== 'completed'
30
+ && turnState.state !== 'interrupted';
31
+ }
32
+ export function resolveTurnMessageSemantics(input) {
33
+ const turnMetadata = normalizeTurnMetadata(input.metadata);
34
+ if (turnMetadata?.turnSemantics)
35
+ return turnMetadata.turnSemantics;
36
+ if (turnMetadata?.turnComplete === true)
37
+ return 'turn_complete';
38
+ if (input.senderType === 'human')
39
+ return 'turn_complete';
40
+ return isTurnOpen(input.senderTurnState) ? 'progress' : 'turn_complete';
41
+ }
42
+ export function shouldPromoteConversationMessage(input) {
43
+ return resolveTurnMessageSemantics(input) !== 'progress';
44
+ }
45
+ export function shouldTriggerAgentTurn(input) {
46
+ const semantics = resolveTurnMessageSemantics(input);
47
+ const turnMetadata = normalizeTurnMetadata(input.metadata);
48
+ if (turnMetadata?.replyBehavior === 'suppress_auto_reply') {
49
+ return { allow: false, semantics };
50
+ }
51
+ if (input.senderType === 'human') {
52
+ return { allow: true, semantics };
53
+ }
54
+ return { allow: semantics !== 'progress', semantics };
55
+ }
56
+ export function normalizeRuntimeTurnState(value) {
57
+ if (!isRecord(value))
58
+ return null;
59
+ if (value.state === 'idle'
60
+ || value.state === 'thinking'
61
+ || value.state === 'streaming'
62
+ || value.state === 'tool'
63
+ || value.state === 'waiting_input'
64
+ || value.state === 'completed'
65
+ || value.state === 'interrupted') {
66
+ return { state: value.state };
67
+ }
68
+ if (value.state === 'running') {
69
+ return { state: 'streaming' };
70
+ }
71
+ if (value.state === 'requires_action') {
72
+ return { state: 'waiting_input' };
73
+ }
74
+ return null;
75
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@canonmsg/backend-contracts",
3
+ "version": "0.1.0",
4
+ "description": "Canon backend contract helpers shared by Functions and stream-service",
5
+ "type": "module",
6
+ "main": "dist/cjs/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/cjs/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node -e \"require('fs').mkdirSync('dist/cjs',{recursive:true}); require('fs').writeFileSync('dist/cjs/package.json', JSON.stringify({type:'commonjs'}))\"",
20
+ "dev": "tsc -p tsconfig.json --watch",
21
+ "test": "vitest run",
22
+ "prepack": "npm run build"
23
+ },
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "keywords": [
28
+ "canon",
29
+ "backend",
30
+ "contracts",
31
+ "wire"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/HeyBobChan/canon",
36
+ "directory": "packages/backend-contracts"
37
+ },
38
+ "homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/backend-contracts",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "typescript": "~5.7.0",
45
+ "vitest": "^3.0.0"
46
+ },
47
+ "license": "MIT"
48
+ }