@canonmsg/backend-contracts 8.3.0 → 8.5.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-verbs.schema.json +0 -1
- package/dist/cjs/conversationMembership.js +48 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/verbSchemas.js +1 -1
- package/dist/cjs/workSessions.js +162 -0
- package/dist/communication.d.ts +16 -4
- package/dist/conversationMembership.d.ts +28 -0
- package/dist/conversationMembership.js +40 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/replyAuthority.d.ts +2 -0
- package/dist/verbContract.d.ts +13 -2
- package/dist/verbSchemas.d.ts +1 -1
- package/dist/verbSchemas.js +1 -1
- package/dist/workSessions.d.ts +156 -0
- package/dist/workSessions.js +150 -0
- package/package.json +7 -9
|
@@ -1427,7 +1427,6 @@
|
|
|
1427
1427
|
"type": "object",
|
|
1428
1428
|
"description": "Create a group conversation. Each target's groupJoinPolicy is enforced server-side with staged admission: directly-addable members join at creation, approval-required members become pending group_invite requests; hard-denied members are skipped (see the result). A creator-only group is valid when at least one requested member has a pending invite. Under MLS, membership changes are Add/Remove proposals + Commit — a group operation is a cryptographic state change, not a codec swap.",
|
|
1429
1429
|
"required": [
|
|
1430
|
-
"name",
|
|
1431
1430
|
"memberIds"
|
|
1432
1431
|
],
|
|
1433
1432
|
"additionalProperties": false,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeConversationRole = normalizeConversationRole;
|
|
4
|
+
exports.isConversationAdminRole = isConversationAdminRole;
|
|
5
|
+
exports.readMembershipRevision = readMembershipRevision;
|
|
6
|
+
exports.resolveConversationPolicyScope = resolveConversationPolicyScope;
|
|
7
|
+
exports.buildConversationParticipantSummary = buildConversationParticipantSummary;
|
|
8
|
+
exports.readConversationInvitationPolicy = readConversationInvitationPolicy;
|
|
9
|
+
const communication_js_1 = require("./communication.js");
|
|
10
|
+
/** `owner` is a read-only legacy spelling, never superior to another admin. */
|
|
11
|
+
function normalizeConversationRole(value) {
|
|
12
|
+
return value === 'admin' || value === 'owner' ? 'admin' : value === 'member' ? 'member' : null;
|
|
13
|
+
}
|
|
14
|
+
function isConversationAdminRole(value) {
|
|
15
|
+
return normalizeConversationRole(value) === 'admin';
|
|
16
|
+
}
|
|
17
|
+
function readMembershipRevision(value) {
|
|
18
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
19
|
+
}
|
|
20
|
+
/** Behavioral scope follows the current roster, never the legacy shape label. */
|
|
21
|
+
function resolveConversationPolicyScope(memberIdsOrCount) {
|
|
22
|
+
const count = Array.isArray(memberIdsOrCount)
|
|
23
|
+
? new Set(memberIdsOrCount.filter((id) => typeof id === 'string' && id.length > 0)).size
|
|
24
|
+
: memberIdsOrCount;
|
|
25
|
+
if (typeof count !== 'number' || !Number.isSafeInteger(count))
|
|
26
|
+
return 'unknown';
|
|
27
|
+
return count === 2 ? 'direct' : count > 2 ? 'group' : 'unknown';
|
|
28
|
+
}
|
|
29
|
+
function buildConversationParticipantSummary(participants) {
|
|
30
|
+
const participantTypes = Object.fromEntries(participants.map(({ userId, userType }) => [userId, userType === 'ai_agent' ? 'ai_agent' : 'human']));
|
|
31
|
+
const types = Object.values(participantTypes);
|
|
32
|
+
const agentCount = types.filter((type) => type === 'ai_agent').length;
|
|
33
|
+
return {
|
|
34
|
+
participantTypes,
|
|
35
|
+
participantSummary: { humanCount: types.length - agentCount, agentCount, totalCount: types.length },
|
|
36
|
+
isAgentChat: agentCount > 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Legacy fields are compatibility inputs to one conservative invitation policy. */
|
|
40
|
+
function readConversationInvitationPolicy(data) {
|
|
41
|
+
const inbound = (0, communication_js_1.readCommunicationRule)(data?.inboundPolicy);
|
|
42
|
+
const group = (0, communication_js_1.readCommunicationRule)(data?.groupJoinPolicy);
|
|
43
|
+
if (inbound === 'closed' || group === 'closed')
|
|
44
|
+
return 'closed';
|
|
45
|
+
if (inbound === 'approval-required' || group === 'approval-required')
|
|
46
|
+
return 'approval-required';
|
|
47
|
+
return 'open';
|
|
48
|
+
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -36,3 +36,5 @@ __exportStar(require("./moderation.js"), exports);
|
|
|
36
36
|
__exportStar(require("./selfContext.js"), exports);
|
|
37
37
|
__exportStar(require("./replyAuthority.js"), exports);
|
|
38
38
|
__exportStar(require("./runtimeDescriptor.js"), exports);
|
|
39
|
+
__exportStar(require("./workSessions.js"), exports);
|
|
40
|
+
__exportStar(require("./conversationMembership.js"), exports);
|
package/dist/cjs/verbSchemas.js
CHANGED
|
@@ -787,7 +787,7 @@ const create_group_input = {
|
|
|
787
787
|
+ 'pending invite. Under MLS, membership '
|
|
788
788
|
+ 'changes are Add/Remove proposals + Commit — a group operation is a '
|
|
789
789
|
+ 'cryptographic state change, not a codec swap.',
|
|
790
|
-
required: ['
|
|
790
|
+
required: ['memberIds'],
|
|
791
791
|
additionalProperties: false,
|
|
792
792
|
properties: {
|
|
793
793
|
name: { type: 'string', minLength: 1, maxLength: verbContract_js_1.VERB_LIMITS.groupNameChars },
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WORK_SESSION_REQUEST_TTL_MS = exports.WORK_SESSION_LEASE_MS = exports.WORK_SESSION_SCHEMA = void 0;
|
|
4
|
+
exports.workSessionId = workSessionId;
|
|
5
|
+
exports.parseWorkSessionSettings = parseWorkSessionSettings;
|
|
6
|
+
exports.parseWorkSessionCatalog = parseWorkSessionCatalog;
|
|
7
|
+
exports.parseWorkSessionSelection = parseWorkSessionSelection;
|
|
8
|
+
exports.assertWorkSessionSelectionAvailable = assertWorkSessionSelectionAvailable;
|
|
9
|
+
exports.parseWorkSessionRuntimeRegistration = parseWorkSessionRuntimeRegistration;
|
|
10
|
+
exports.parseWorkSessionRuntimeAuth = parseWorkSessionRuntimeAuth;
|
|
11
|
+
exports.parseRequestWorkSessionInput = parseRequestWorkSessionInput;
|
|
12
|
+
exports.parseWorkSessionCompletion = parseWorkSessionCompletion;
|
|
13
|
+
/** Private owner/operator workflow. None of this catalog belongs in public runtime descriptors. */
|
|
14
|
+
exports.WORK_SESSION_SCHEMA = 'canon.work-sessions.v1';
|
|
15
|
+
exports.WORK_SESSION_LEASE_MS = 90_000;
|
|
16
|
+
exports.WORK_SESSION_REQUEST_TTL_MS = 5 * 60_000;
|
|
17
|
+
const SETTINGS_KEYS = ['projectId', 'modelId', 'reasoningEffort', 'permissionMode', 'executionMode'];
|
|
18
|
+
function object(value, keys, label) {
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
20
|
+
throw new Error(`Invalid ${label}`);
|
|
21
|
+
const result = value;
|
|
22
|
+
if (Object.keys(result).some((key) => !keys.includes(key)))
|
|
23
|
+
throw new Error(`Unsupported ${label} field`);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
function workSessionId(value, label = 'identifier') {
|
|
27
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_.:-]{1,160}$/.test(value))
|
|
28
|
+
throw new Error(`Invalid ${label}`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function label(value, name, max = 160) {
|
|
32
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max)
|
|
33
|
+
throw new Error(`Invalid ${name}`);
|
|
34
|
+
return value.trim();
|
|
35
|
+
}
|
|
36
|
+
function parseWorkSessionSettings(value) {
|
|
37
|
+
const input = object(value, SETTINGS_KEYS, 'settings');
|
|
38
|
+
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, workSessionId(value, key)]));
|
|
39
|
+
}
|
|
40
|
+
function choices(value, models = false) {
|
|
41
|
+
if (!Array.isArray(value) || value.length > 128)
|
|
42
|
+
throw new Error('Invalid choices');
|
|
43
|
+
const ids = new Set();
|
|
44
|
+
return value.map((entry) => {
|
|
45
|
+
const input = object(entry, ['id', 'label', 'description', ...(models ? ['reasoningEfforts', 'defaultReasoningEffort'] : [])], 'choice');
|
|
46
|
+
const id = workSessionId(input.id);
|
|
47
|
+
if (ids.has(id))
|
|
48
|
+
throw new Error('Duplicate choice identifier');
|
|
49
|
+
ids.add(id);
|
|
50
|
+
const result = { id, label: label(input.label, 'choice label') };
|
|
51
|
+
if (input.description !== undefined)
|
|
52
|
+
result.description = label(input.description, 'choice description', 512);
|
|
53
|
+
if (input.reasoningEfforts !== undefined)
|
|
54
|
+
result.reasoningEfforts = choices(input.reasoningEfforts);
|
|
55
|
+
if (input.defaultReasoningEffort !== undefined) {
|
|
56
|
+
result.defaultReasoningEffort = workSessionId(input.defaultReasoningEffort);
|
|
57
|
+
if (!result.reasoningEfforts?.some((choice) => choice.id === result.defaultReasoningEffort))
|
|
58
|
+
throw new Error('Invalid default reasoning effort');
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function parseWorkSessionCatalog(value) {
|
|
64
|
+
const input = object(value, ['revision', 'provider', 'canCreate', 'canAttach', 'projects', 'models', 'reasoningEfforts', 'permissionModes', 'executionModes', 'defaults', 'sessions'], 'catalog');
|
|
65
|
+
if (typeof input.canCreate !== 'boolean' || typeof input.canAttach !== 'boolean')
|
|
66
|
+
throw new Error('Invalid catalog capabilities');
|
|
67
|
+
if (!Array.isArray(input.sessions) || input.sessions.length > 128)
|
|
68
|
+
throw new Error('Invalid loaded sessions');
|
|
69
|
+
const ids = new Set();
|
|
70
|
+
const result = {
|
|
71
|
+
revision: workSessionId(input.revision, 'catalog revision'), provider: workSessionId(input.provider, 'provider'),
|
|
72
|
+
canCreate: input.canCreate, canAttach: input.canAttach, projects: choices(input.projects),
|
|
73
|
+
sessions: input.sessions.map((entry) => {
|
|
74
|
+
const session = object(entry, ['id', 'title', 'status', 'settings', 'projectLabel', 'modelLabel'], 'loaded session');
|
|
75
|
+
const id = workSessionId(session.id, 'session identifier');
|
|
76
|
+
if (ids.has(id))
|
|
77
|
+
throw new Error('Duplicate loaded session');
|
|
78
|
+
ids.add(id);
|
|
79
|
+
if (session.status !== 'idle' && session.status !== 'running')
|
|
80
|
+
throw new Error('Invalid session status');
|
|
81
|
+
return {
|
|
82
|
+
id, title: label(session.title, 'session title'), status: session.status,
|
|
83
|
+
settings: parseWorkSessionSettings(session.settings),
|
|
84
|
+
...(session.projectLabel !== undefined ? { projectLabel: label(session.projectLabel, 'project label') } : {}),
|
|
85
|
+
...(session.modelLabel !== undefined ? { modelLabel: label(session.modelLabel, 'model label') } : {}),
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
for (const key of ['models', 'reasoningEfforts', 'permissionModes', 'executionModes']) {
|
|
90
|
+
if (input[key] !== undefined)
|
|
91
|
+
result[key] = choices(input[key], key === 'models');
|
|
92
|
+
}
|
|
93
|
+
if (input.defaults !== undefined)
|
|
94
|
+
result.defaults = parseWorkSessionSettings(input.defaults);
|
|
95
|
+
if (result.canCreate && !result.projects.length)
|
|
96
|
+
throw new Error('Creating sessions requires an advertised project');
|
|
97
|
+
if (JSON.stringify(result).length > 65_536)
|
|
98
|
+
throw new Error('Work session catalog is too large');
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
function parseWorkSessionSelection(value) {
|
|
102
|
+
const mode = value?.mode;
|
|
103
|
+
if (mode === 'attach') {
|
|
104
|
+
const input = object(value, ['mode', 'sessionId'], 'attach selection');
|
|
105
|
+
return { mode, sessionId: workSessionId(input.sessionId, 'session identifier') };
|
|
106
|
+
}
|
|
107
|
+
if (mode === 'create') {
|
|
108
|
+
const { mode: _mode, ...settings } = object(value, ['mode', ...SETTINGS_KEYS], 'create selection');
|
|
109
|
+
return { mode, ...parseWorkSessionSettings(settings), projectId: workSessionId(settings.projectId, 'project identifier') };
|
|
110
|
+
}
|
|
111
|
+
throw new Error('Invalid work session selection');
|
|
112
|
+
}
|
|
113
|
+
function assertWorkSessionSelectionAvailable(selection, catalog) {
|
|
114
|
+
if (selection.mode === 'attach') {
|
|
115
|
+
if (!catalog.canAttach || !catalog.sessions.some((session) => session.id === selection.sessionId))
|
|
116
|
+
throw new Error('Selected loaded session is unavailable');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!catalog.canCreate || !catalog.projects.some((project) => project.id === selection.projectId))
|
|
120
|
+
throw new Error('Selected project is unavailable');
|
|
121
|
+
const modelId = selection.modelId ?? catalog.defaults?.modelId;
|
|
122
|
+
const model = catalog.models?.find((choice) => choice.id === modelId);
|
|
123
|
+
const available = {
|
|
124
|
+
modelId: catalog.models, reasoningEffort: model?.reasoningEfforts ?? catalog.reasoningEfforts,
|
|
125
|
+
permissionMode: catalog.permissionModes, executionMode: catalog.executionModes,
|
|
126
|
+
};
|
|
127
|
+
for (const key of ['modelId', 'reasoningEffort', 'permissionMode', 'executionMode']) {
|
|
128
|
+
if (selection[key] !== undefined && !available[key]?.some((choice) => choice.id === selection[key]))
|
|
129
|
+
throw new Error(`Selected ${key} is unavailable`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function parseWorkSessionRuntimeRegistration(value) {
|
|
133
|
+
const input = object(value, ['hostId', 'runtimeEpoch', 'displayName', 'catalog'], 'runtime registration');
|
|
134
|
+
return { hostId: workSessionId(input.hostId), runtimeEpoch: workSessionId(input.runtimeEpoch), displayName: label(input.displayName, 'host name'), catalog: parseWorkSessionCatalog(input.catalog) };
|
|
135
|
+
}
|
|
136
|
+
function parseWorkSessionRuntimeAuth(value, extraKeys = []) {
|
|
137
|
+
const input = object(value, ['hostId', 'runtimeEpoch', 'leaseToken', ...extraKeys], 'runtime request');
|
|
138
|
+
return { hostId: workSessionId(input.hostId), runtimeEpoch: workSessionId(input.runtimeEpoch), leaseToken: workSessionId(input.leaseToken, 'lease token') };
|
|
139
|
+
}
|
|
140
|
+
function parseRequestWorkSessionInput(value) {
|
|
141
|
+
const input = object(value, ['agentId', 'conversationId', 'requestId', 'hostId', 'runtimeEpoch', 'catalogRevision', 'selection'], 'work session request');
|
|
142
|
+
if (typeof input.requestId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.requestId))
|
|
143
|
+
throw new Error('requestId must be a UUID');
|
|
144
|
+
return {
|
|
145
|
+
agentId: workSessionId(input.agentId), conversationId: workSessionId(input.conversationId), requestId: input.requestId,
|
|
146
|
+
hostId: workSessionId(input.hostId), runtimeEpoch: workSessionId(input.runtimeEpoch), catalogRevision: workSessionId(input.catalogRevision),
|
|
147
|
+
selection: parseWorkSessionSelection(input.selection),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function parseWorkSessionCompletion(value) {
|
|
151
|
+
const status = value?.status;
|
|
152
|
+
if (status === 'attached') {
|
|
153
|
+
const input = object(value, ['status', 'nativeSessionId', 'settings'], 'attached result');
|
|
154
|
+
return { status, nativeSessionId: workSessionId(input.nativeSessionId), settings: parseWorkSessionSettings(input.settings) };
|
|
155
|
+
}
|
|
156
|
+
if (status === 'failed' || status === 'uncertain') {
|
|
157
|
+
const input = object(value, ['status', 'error'], 'failure result');
|
|
158
|
+
const error = object(input.error, ['code', 'message'], 'failure');
|
|
159
|
+
return { status, error: { code: workSessionId(error.code), message: label(error.message, 'failure message', 512) } };
|
|
160
|
+
}
|
|
161
|
+
throw new Error('Invalid work session result');
|
|
162
|
+
}
|
package/dist/communication.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ListConversationsResult } from './verbContract.js';
|
|
1
2
|
import type { DiscoverAgentsInput, DiscoverAgentsResult } from './agentDirectory.js';
|
|
2
3
|
/** The deliberately small policy vocabulary for principal communication. */
|
|
3
4
|
export type CommunicationRule = 'open' | 'approval-required' | 'closed';
|
|
@@ -20,14 +21,14 @@ export type CommunicateInput = ({
|
|
|
20
21
|
text: string;
|
|
21
22
|
messageId?: string;
|
|
22
23
|
} | {
|
|
23
|
-
action: 'start_direct';
|
|
24
|
+
action: 'start_direct' | 'start_conversation';
|
|
24
25
|
principalId: string;
|
|
25
26
|
text: string;
|
|
26
27
|
selection?: DirectConversationSelection;
|
|
27
28
|
messageId?: string;
|
|
28
29
|
} | {
|
|
29
|
-
action: 'create_group';
|
|
30
|
-
name
|
|
30
|
+
action: 'create_group' | 'create_conversation';
|
|
31
|
+
name?: string;
|
|
31
32
|
memberIds: string[];
|
|
32
33
|
} | {
|
|
33
34
|
action: 'forward_message';
|
|
@@ -42,12 +43,23 @@ export type CommunicateInput = ({
|
|
|
42
43
|
text?: string;
|
|
43
44
|
messageId?: string;
|
|
44
45
|
} | {
|
|
45
|
-
action: 'manage_group_members';
|
|
46
|
+
action: 'manage_group_members' | 'manage_participants';
|
|
46
47
|
conversationId: string;
|
|
47
48
|
userId: string;
|
|
48
49
|
operation: 'add' | 'remove';
|
|
50
|
+
} | {
|
|
51
|
+
action: 'list_conversations';
|
|
52
|
+
limit?: number;
|
|
53
|
+
} | {
|
|
54
|
+
action: 'leave_conversation';
|
|
55
|
+
conversationId: string;
|
|
49
56
|
};
|
|
50
57
|
export type CommunicateResult = ({
|
|
58
|
+
status: 'listed';
|
|
59
|
+
} & ListConversationsResult) | {
|
|
60
|
+
status: 'left';
|
|
61
|
+
conversationId: string;
|
|
62
|
+
} | ({
|
|
51
63
|
status: 'discovered';
|
|
52
64
|
} & DiscoverAgentsResult) | {
|
|
53
65
|
status: 'messaged';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CommunicationRule } from './communication.js';
|
|
2
|
+
/** Shared social membership semantics; presentation and runtime ownership are separate. */
|
|
3
|
+
export type ConversationRole = 'admin' | 'member';
|
|
4
|
+
export type ConversationParticipantType = 'human' | 'ai_agent';
|
|
5
|
+
export interface ConversationParticipantSummary {
|
|
6
|
+
humanCount: number;
|
|
7
|
+
agentCount: number;
|
|
8
|
+
totalCount: number;
|
|
9
|
+
}
|
|
10
|
+
/** `owner` is a read-only legacy spelling, never superior to another admin. */
|
|
11
|
+
export declare function normalizeConversationRole(value: unknown): ConversationRole | null;
|
|
12
|
+
export declare function isConversationAdminRole(value: unknown): boolean;
|
|
13
|
+
export declare function readMembershipRevision(value: unknown): number;
|
|
14
|
+
/** Behavioral scope follows the current roster, never the legacy shape label. */
|
|
15
|
+
export declare function resolveConversationPolicyScope(memberIdsOrCount: unknown): 'direct' | 'group' | 'unknown';
|
|
16
|
+
export declare function buildConversationParticipantSummary(participants: readonly {
|
|
17
|
+
userId: string;
|
|
18
|
+
userType: unknown;
|
|
19
|
+
}[]): {
|
|
20
|
+
participantTypes: Record<string, ConversationParticipantType>;
|
|
21
|
+
participantSummary: ConversationParticipantSummary;
|
|
22
|
+
isAgentChat: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** Legacy fields are compatibility inputs to one conservative invitation policy. */
|
|
25
|
+
export declare function readConversationInvitationPolicy(data: {
|
|
26
|
+
inboundPolicy?: unknown;
|
|
27
|
+
groupJoinPolicy?: unknown;
|
|
28
|
+
} | null | undefined): CommunicationRule;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readCommunicationRule } from './communication.js';
|
|
2
|
+
/** `owner` is a read-only legacy spelling, never superior to another admin. */
|
|
3
|
+
export function normalizeConversationRole(value) {
|
|
4
|
+
return value === 'admin' || value === 'owner' ? 'admin' : value === 'member' ? 'member' : null;
|
|
5
|
+
}
|
|
6
|
+
export function isConversationAdminRole(value) {
|
|
7
|
+
return normalizeConversationRole(value) === 'admin';
|
|
8
|
+
}
|
|
9
|
+
export function readMembershipRevision(value) {
|
|
10
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
11
|
+
}
|
|
12
|
+
/** Behavioral scope follows the current roster, never the legacy shape label. */
|
|
13
|
+
export function resolveConversationPolicyScope(memberIdsOrCount) {
|
|
14
|
+
const count = Array.isArray(memberIdsOrCount)
|
|
15
|
+
? new Set(memberIdsOrCount.filter((id) => typeof id === 'string' && id.length > 0)).size
|
|
16
|
+
: memberIdsOrCount;
|
|
17
|
+
if (typeof count !== 'number' || !Number.isSafeInteger(count))
|
|
18
|
+
return 'unknown';
|
|
19
|
+
return count === 2 ? 'direct' : count > 2 ? 'group' : 'unknown';
|
|
20
|
+
}
|
|
21
|
+
export function buildConversationParticipantSummary(participants) {
|
|
22
|
+
const participantTypes = Object.fromEntries(participants.map(({ userId, userType }) => [userId, userType === 'ai_agent' ? 'ai_agent' : 'human']));
|
|
23
|
+
const types = Object.values(participantTypes);
|
|
24
|
+
const agentCount = types.filter((type) => type === 'ai_agent').length;
|
|
25
|
+
return {
|
|
26
|
+
participantTypes,
|
|
27
|
+
participantSummary: { humanCount: types.length - agentCount, agentCount, totalCount: types.length },
|
|
28
|
+
isAgentChat: agentCount > 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Legacy fields are compatibility inputs to one conservative invitation policy. */
|
|
32
|
+
export function readConversationInvitationPolicy(data) {
|
|
33
|
+
const inbound = readCommunicationRule(data?.inboundPolicy);
|
|
34
|
+
const group = readCommunicationRule(data?.groupJoinPolicy);
|
|
35
|
+
if (inbound === 'closed' || group === 'closed')
|
|
36
|
+
return 'closed';
|
|
37
|
+
if (inbound === 'approval-required' || group === 'approval-required')
|
|
38
|
+
return 'approval-required';
|
|
39
|
+
return 'open';
|
|
40
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/replyAuthority.d.ts
CHANGED
package/dist/verbContract.d.ts
CHANGED
|
@@ -580,8 +580,8 @@ export interface ForwardResult {
|
|
|
580
580
|
forwardedFrom?: unknown;
|
|
581
581
|
}
|
|
582
582
|
export interface CreateGroupInput {
|
|
583
|
-
/**
|
|
584
|
-
name
|
|
583
|
+
/** Optional conversation title (authoring cap VERB_LIMITS.groupNameChars). */
|
|
584
|
+
name?: string;
|
|
585
585
|
/** Other members; the caller is added automatically. Cap
|
|
586
586
|
* VERB_LIMITS.groupMembers including the creator. Each target's
|
|
587
587
|
* groupJoinPolicy is enforced server-side. */
|
|
@@ -658,6 +658,17 @@ export interface VerbConversationSummary {
|
|
|
658
658
|
name?: string | null;
|
|
659
659
|
topic: string | null;
|
|
660
660
|
memberIds: string[];
|
|
661
|
+
membershipModel?: 'unified';
|
|
662
|
+
membershipRevision?: number;
|
|
663
|
+
participantTypes?: Record<string, 'human' | 'ai_agent'>;
|
|
664
|
+
participantSummary?: {
|
|
665
|
+
humanCount: number;
|
|
666
|
+
agentCount: number;
|
|
667
|
+
totalCount: number;
|
|
668
|
+
};
|
|
669
|
+
runtimeSessionKind?: 'direct' | 'group';
|
|
670
|
+
admissionId?: string | null;
|
|
671
|
+
historyStartAt?: string | null;
|
|
661
672
|
isAgentChat: boolean;
|
|
662
673
|
hasUnread?: boolean;
|
|
663
674
|
lastMessage: {
|
package/dist/verbSchemas.d.ts
CHANGED
|
@@ -1298,7 +1298,7 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
|
|
|
1298
1298
|
readonly create_group_input: {
|
|
1299
1299
|
readonly type: "object";
|
|
1300
1300
|
readonly description: string;
|
|
1301
|
-
readonly required: readonly ["
|
|
1301
|
+
readonly required: readonly ["memberIds"];
|
|
1302
1302
|
readonly additionalProperties: false;
|
|
1303
1303
|
readonly properties: {
|
|
1304
1304
|
readonly name: {
|
package/dist/verbSchemas.js
CHANGED
|
@@ -782,7 +782,7 @@ const create_group_input = {
|
|
|
782
782
|
+ 'pending invite. Under MLS, membership '
|
|
783
783
|
+ 'changes are Add/Remove proposals + Commit — a group operation is a '
|
|
784
784
|
+ 'cryptographic state change, not a codec swap.',
|
|
785
|
-
required: ['
|
|
785
|
+
required: ['memberIds'],
|
|
786
786
|
additionalProperties: false,
|
|
787
787
|
properties: {
|
|
788
788
|
name: { type: 'string', minLength: 1, maxLength: VERB_LIMITS.groupNameChars },
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Private owner/operator workflow. None of this catalog belongs in public runtime descriptors. */
|
|
2
|
+
export declare const WORK_SESSION_SCHEMA: "canon.work-sessions.v1";
|
|
3
|
+
export declare const WORK_SESSION_LEASE_MS = 90000;
|
|
4
|
+
export declare const WORK_SESSION_REQUEST_TTL_MS: number;
|
|
5
|
+
export interface WorkSessionChoice {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface WorkSessionModelChoice extends WorkSessionChoice {
|
|
11
|
+
reasoningEfforts?: WorkSessionChoice[];
|
|
12
|
+
defaultReasoningEffort?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface WorkSessionSettings {
|
|
15
|
+
projectId?: string;
|
|
16
|
+
modelId?: string;
|
|
17
|
+
reasoningEffort?: string;
|
|
18
|
+
permissionMode?: string;
|
|
19
|
+
executionMode?: string;
|
|
20
|
+
}
|
|
21
|
+
export type WorkSessionSelection = ({
|
|
22
|
+
mode: 'create';
|
|
23
|
+
projectId: string;
|
|
24
|
+
} & Omit<WorkSessionSettings, 'projectId'>) | {
|
|
25
|
+
mode: 'attach';
|
|
26
|
+
sessionId: string;
|
|
27
|
+
};
|
|
28
|
+
export interface WorkSessionLoadedSession {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
status: 'idle' | 'running';
|
|
32
|
+
settings: WorkSessionSettings;
|
|
33
|
+
projectLabel?: string;
|
|
34
|
+
modelLabel?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface WorkSessionCatalog {
|
|
37
|
+
revision: string;
|
|
38
|
+
provider: string;
|
|
39
|
+
canCreate: boolean;
|
|
40
|
+
canAttach: boolean;
|
|
41
|
+
projects: WorkSessionChoice[];
|
|
42
|
+
models?: WorkSessionModelChoice[];
|
|
43
|
+
reasoningEfforts?: WorkSessionChoice[];
|
|
44
|
+
permissionModes?: WorkSessionChoice[];
|
|
45
|
+
executionModes?: WorkSessionChoice[];
|
|
46
|
+
defaults?: WorkSessionSettings;
|
|
47
|
+
sessions: WorkSessionLoadedSession[];
|
|
48
|
+
}
|
|
49
|
+
export interface WorkSessionRuntimeIdentity {
|
|
50
|
+
hostId: string;
|
|
51
|
+
runtimeEpoch: string;
|
|
52
|
+
}
|
|
53
|
+
export interface WorkSessionRuntimeLease extends WorkSessionRuntimeIdentity {
|
|
54
|
+
/** Agent-only fencing credential. Never expose in the owner catalog or request status. */
|
|
55
|
+
leaseToken: string;
|
|
56
|
+
expiresAt: number;
|
|
57
|
+
}
|
|
58
|
+
export interface WorkSessionRuntimeRegistration extends WorkSessionRuntimeIdentity {
|
|
59
|
+
displayName: string;
|
|
60
|
+
catalog: WorkSessionCatalog;
|
|
61
|
+
}
|
|
62
|
+
export interface WorkSessionBinding {
|
|
63
|
+
workSessionId: string;
|
|
64
|
+
conversationId: string;
|
|
65
|
+
hostId: string;
|
|
66
|
+
provider: string;
|
|
67
|
+
nativeSessionId: string;
|
|
68
|
+
settings: WorkSessionSettings;
|
|
69
|
+
createdAt: number;
|
|
70
|
+
}
|
|
71
|
+
export interface WorkSessionRuntimeState {
|
|
72
|
+
ownerId: string;
|
|
73
|
+
lease: WorkSessionRuntimeLease;
|
|
74
|
+
bindings: WorkSessionBinding[];
|
|
75
|
+
}
|
|
76
|
+
export interface WorkSessionCatalogResult {
|
|
77
|
+
status: 'available' | 'offline' | 'unavailable';
|
|
78
|
+
agentId: string;
|
|
79
|
+
hostId?: string;
|
|
80
|
+
runtimeEpoch?: string;
|
|
81
|
+
displayName?: string;
|
|
82
|
+
expiresAt?: number;
|
|
83
|
+
catalog?: WorkSessionCatalog;
|
|
84
|
+
bindings: WorkSessionBinding[];
|
|
85
|
+
}
|
|
86
|
+
export interface RequestWorkSessionInput extends WorkSessionRuntimeIdentity {
|
|
87
|
+
agentId: string;
|
|
88
|
+
conversationId: string;
|
|
89
|
+
requestId: string;
|
|
90
|
+
catalogRevision: string;
|
|
91
|
+
selection: WorkSessionSelection;
|
|
92
|
+
}
|
|
93
|
+
export type WorkSessionRequestStatus = 'pending' | 'claimed' | 'attached' | 'failed' | 'uncertain' | 'expired';
|
|
94
|
+
export interface WorkSessionRequest extends RequestWorkSessionInput {
|
|
95
|
+
schema: typeof WORK_SESSION_SCHEMA;
|
|
96
|
+
requestedBy: string;
|
|
97
|
+
status: WorkSessionRequestStatus;
|
|
98
|
+
createdAt: number;
|
|
99
|
+
updatedAt: number;
|
|
100
|
+
expiresAt: number;
|
|
101
|
+
binding?: WorkSessionBinding;
|
|
102
|
+
error?: {
|
|
103
|
+
code: string;
|
|
104
|
+
message: string;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export type WorkSessionCompletion = {
|
|
108
|
+
status: 'attached';
|
|
109
|
+
nativeSessionId: string;
|
|
110
|
+
settings: WorkSessionSettings;
|
|
111
|
+
} | {
|
|
112
|
+
status: 'failed' | 'uncertain';
|
|
113
|
+
error: {
|
|
114
|
+
code: string;
|
|
115
|
+
message: string;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
export type WorkSessionRuntimeAuth = Pick<WorkSessionRuntimeLease, 'hostId' | 'runtimeEpoch' | 'leaseToken'>;
|
|
119
|
+
export interface WorkSessionClaimInput extends WorkSessionRuntimeAuth {
|
|
120
|
+
requestId?: string;
|
|
121
|
+
}
|
|
122
|
+
export interface WorkSessionClaimResult {
|
|
123
|
+
request: WorkSessionRequest | null;
|
|
124
|
+
/** Previously claimed: reconcile the durable host journal, never blindly execute again. */
|
|
125
|
+
replayed: boolean;
|
|
126
|
+
}
|
|
127
|
+
export interface CompleteWorkSessionInput extends WorkSessionRuntimeAuth {
|
|
128
|
+
requestId: string;
|
|
129
|
+
result: WorkSessionCompletion;
|
|
130
|
+
}
|
|
131
|
+
export interface WorkSessionHeartbeatInput extends WorkSessionRuntimeAuth {
|
|
132
|
+
catalog?: WorkSessionCatalog;
|
|
133
|
+
}
|
|
134
|
+
export interface ReleaseWorkSessionBindingInput extends WorkSessionRuntimeAuth {
|
|
135
|
+
conversationId: string;
|
|
136
|
+
workSessionId: string;
|
|
137
|
+
}
|
|
138
|
+
export interface ResolveWorkSessionRequestInput {
|
|
139
|
+
agentId: string;
|
|
140
|
+
requestId: string;
|
|
141
|
+
}
|
|
142
|
+
export interface GetWorkSessionRequestInput {
|
|
143
|
+
agentId: string;
|
|
144
|
+
/** Exactly one selector. Conversation lookup recovers the latest operation after reload. */
|
|
145
|
+
requestId?: string;
|
|
146
|
+
conversationId?: string;
|
|
147
|
+
}
|
|
148
|
+
export declare function workSessionId(value: unknown, label?: string): string;
|
|
149
|
+
export declare function parseWorkSessionSettings(value: unknown): WorkSessionSettings;
|
|
150
|
+
export declare function parseWorkSessionCatalog(value: unknown): WorkSessionCatalog;
|
|
151
|
+
export declare function parseWorkSessionSelection(value: unknown): WorkSessionSelection;
|
|
152
|
+
export declare function assertWorkSessionSelectionAvailable(selection: WorkSessionSelection, catalog: WorkSessionCatalog): void;
|
|
153
|
+
export declare function parseWorkSessionRuntimeRegistration(value: unknown): WorkSessionRuntimeRegistration;
|
|
154
|
+
export declare function parseWorkSessionRuntimeAuth(value: unknown, extraKeys?: string[]): WorkSessionRuntimeAuth;
|
|
155
|
+
export declare function parseRequestWorkSessionInput(value: unknown): RequestWorkSessionInput;
|
|
156
|
+
export declare function parseWorkSessionCompletion(value: unknown): WorkSessionCompletion;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Private owner/operator workflow. None of this catalog belongs in public runtime descriptors. */
|
|
2
|
+
export const WORK_SESSION_SCHEMA = 'canon.work-sessions.v1';
|
|
3
|
+
export const WORK_SESSION_LEASE_MS = 90_000;
|
|
4
|
+
export const WORK_SESSION_REQUEST_TTL_MS = 5 * 60_000;
|
|
5
|
+
const SETTINGS_KEYS = ['projectId', 'modelId', 'reasoningEffort', 'permissionMode', 'executionMode'];
|
|
6
|
+
function object(value, keys, label) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
|
+
throw new Error(`Invalid ${label}`);
|
|
9
|
+
const result = value;
|
|
10
|
+
if (Object.keys(result).some((key) => !keys.includes(key)))
|
|
11
|
+
throw new Error(`Unsupported ${label} field`);
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
export function workSessionId(value, label = 'identifier') {
|
|
15
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_.:-]{1,160}$/.test(value))
|
|
16
|
+
throw new Error(`Invalid ${label}`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function label(value, name, max = 160) {
|
|
20
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max)
|
|
21
|
+
throw new Error(`Invalid ${name}`);
|
|
22
|
+
return value.trim();
|
|
23
|
+
}
|
|
24
|
+
export function parseWorkSessionSettings(value) {
|
|
25
|
+
const input = object(value, SETTINGS_KEYS, 'settings');
|
|
26
|
+
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, workSessionId(value, key)]));
|
|
27
|
+
}
|
|
28
|
+
function choices(value, models = false) {
|
|
29
|
+
if (!Array.isArray(value) || value.length > 128)
|
|
30
|
+
throw new Error('Invalid choices');
|
|
31
|
+
const ids = new Set();
|
|
32
|
+
return value.map((entry) => {
|
|
33
|
+
const input = object(entry, ['id', 'label', 'description', ...(models ? ['reasoningEfforts', 'defaultReasoningEffort'] : [])], 'choice');
|
|
34
|
+
const id = workSessionId(input.id);
|
|
35
|
+
if (ids.has(id))
|
|
36
|
+
throw new Error('Duplicate choice identifier');
|
|
37
|
+
ids.add(id);
|
|
38
|
+
const result = { id, label: label(input.label, 'choice label') };
|
|
39
|
+
if (input.description !== undefined)
|
|
40
|
+
result.description = label(input.description, 'choice description', 512);
|
|
41
|
+
if (input.reasoningEfforts !== undefined)
|
|
42
|
+
result.reasoningEfforts = choices(input.reasoningEfforts);
|
|
43
|
+
if (input.defaultReasoningEffort !== undefined) {
|
|
44
|
+
result.defaultReasoningEffort = workSessionId(input.defaultReasoningEffort);
|
|
45
|
+
if (!result.reasoningEfforts?.some((choice) => choice.id === result.defaultReasoningEffort))
|
|
46
|
+
throw new Error('Invalid default reasoning effort');
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function parseWorkSessionCatalog(value) {
|
|
52
|
+
const input = object(value, ['revision', 'provider', 'canCreate', 'canAttach', 'projects', 'models', 'reasoningEfforts', 'permissionModes', 'executionModes', 'defaults', 'sessions'], 'catalog');
|
|
53
|
+
if (typeof input.canCreate !== 'boolean' || typeof input.canAttach !== 'boolean')
|
|
54
|
+
throw new Error('Invalid catalog capabilities');
|
|
55
|
+
if (!Array.isArray(input.sessions) || input.sessions.length > 128)
|
|
56
|
+
throw new Error('Invalid loaded sessions');
|
|
57
|
+
const ids = new Set();
|
|
58
|
+
const result = {
|
|
59
|
+
revision: workSessionId(input.revision, 'catalog revision'), provider: workSessionId(input.provider, 'provider'),
|
|
60
|
+
canCreate: input.canCreate, canAttach: input.canAttach, projects: choices(input.projects),
|
|
61
|
+
sessions: input.sessions.map((entry) => {
|
|
62
|
+
const session = object(entry, ['id', 'title', 'status', 'settings', 'projectLabel', 'modelLabel'], 'loaded session');
|
|
63
|
+
const id = workSessionId(session.id, 'session identifier');
|
|
64
|
+
if (ids.has(id))
|
|
65
|
+
throw new Error('Duplicate loaded session');
|
|
66
|
+
ids.add(id);
|
|
67
|
+
if (session.status !== 'idle' && session.status !== 'running')
|
|
68
|
+
throw new Error('Invalid session status');
|
|
69
|
+
return {
|
|
70
|
+
id, title: label(session.title, 'session title'), status: session.status,
|
|
71
|
+
settings: parseWorkSessionSettings(session.settings),
|
|
72
|
+
...(session.projectLabel !== undefined ? { projectLabel: label(session.projectLabel, 'project label') } : {}),
|
|
73
|
+
...(session.modelLabel !== undefined ? { modelLabel: label(session.modelLabel, 'model label') } : {}),
|
|
74
|
+
};
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
for (const key of ['models', 'reasoningEfforts', 'permissionModes', 'executionModes']) {
|
|
78
|
+
if (input[key] !== undefined)
|
|
79
|
+
result[key] = choices(input[key], key === 'models');
|
|
80
|
+
}
|
|
81
|
+
if (input.defaults !== undefined)
|
|
82
|
+
result.defaults = parseWorkSessionSettings(input.defaults);
|
|
83
|
+
if (result.canCreate && !result.projects.length)
|
|
84
|
+
throw new Error('Creating sessions requires an advertised project');
|
|
85
|
+
if (JSON.stringify(result).length > 65_536)
|
|
86
|
+
throw new Error('Work session catalog is too large');
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
export function parseWorkSessionSelection(value) {
|
|
90
|
+
const mode = value?.mode;
|
|
91
|
+
if (mode === 'attach') {
|
|
92
|
+
const input = object(value, ['mode', 'sessionId'], 'attach selection');
|
|
93
|
+
return { mode, sessionId: workSessionId(input.sessionId, 'session identifier') };
|
|
94
|
+
}
|
|
95
|
+
if (mode === 'create') {
|
|
96
|
+
const { mode: _mode, ...settings } = object(value, ['mode', ...SETTINGS_KEYS], 'create selection');
|
|
97
|
+
return { mode, ...parseWorkSessionSettings(settings), projectId: workSessionId(settings.projectId, 'project identifier') };
|
|
98
|
+
}
|
|
99
|
+
throw new Error('Invalid work session selection');
|
|
100
|
+
}
|
|
101
|
+
export function assertWorkSessionSelectionAvailable(selection, catalog) {
|
|
102
|
+
if (selection.mode === 'attach') {
|
|
103
|
+
if (!catalog.canAttach || !catalog.sessions.some((session) => session.id === selection.sessionId))
|
|
104
|
+
throw new Error('Selected loaded session is unavailable');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!catalog.canCreate || !catalog.projects.some((project) => project.id === selection.projectId))
|
|
108
|
+
throw new Error('Selected project is unavailable');
|
|
109
|
+
const modelId = selection.modelId ?? catalog.defaults?.modelId;
|
|
110
|
+
const model = catalog.models?.find((choice) => choice.id === modelId);
|
|
111
|
+
const available = {
|
|
112
|
+
modelId: catalog.models, reasoningEffort: model?.reasoningEfforts ?? catalog.reasoningEfforts,
|
|
113
|
+
permissionMode: catalog.permissionModes, executionMode: catalog.executionModes,
|
|
114
|
+
};
|
|
115
|
+
for (const key of ['modelId', 'reasoningEffort', 'permissionMode', 'executionMode']) {
|
|
116
|
+
if (selection[key] !== undefined && !available[key]?.some((choice) => choice.id === selection[key]))
|
|
117
|
+
throw new Error(`Selected ${key} is unavailable`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function parseWorkSessionRuntimeRegistration(value) {
|
|
121
|
+
const input = object(value, ['hostId', 'runtimeEpoch', 'displayName', 'catalog'], 'runtime registration');
|
|
122
|
+
return { hostId: workSessionId(input.hostId), runtimeEpoch: workSessionId(input.runtimeEpoch), displayName: label(input.displayName, 'host name'), catalog: parseWorkSessionCatalog(input.catalog) };
|
|
123
|
+
}
|
|
124
|
+
export function parseWorkSessionRuntimeAuth(value, extraKeys = []) {
|
|
125
|
+
const input = object(value, ['hostId', 'runtimeEpoch', 'leaseToken', ...extraKeys], 'runtime request');
|
|
126
|
+
return { hostId: workSessionId(input.hostId), runtimeEpoch: workSessionId(input.runtimeEpoch), leaseToken: workSessionId(input.leaseToken, 'lease token') };
|
|
127
|
+
}
|
|
128
|
+
export function parseRequestWorkSessionInput(value) {
|
|
129
|
+
const input = object(value, ['agentId', 'conversationId', 'requestId', 'hostId', 'runtimeEpoch', 'catalogRevision', 'selection'], 'work session request');
|
|
130
|
+
if (typeof input.requestId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.requestId))
|
|
131
|
+
throw new Error('requestId must be a UUID');
|
|
132
|
+
return {
|
|
133
|
+
agentId: workSessionId(input.agentId), conversationId: workSessionId(input.conversationId), requestId: input.requestId,
|
|
134
|
+
hostId: workSessionId(input.hostId), runtimeEpoch: workSessionId(input.runtimeEpoch), catalogRevision: workSessionId(input.catalogRevision),
|
|
135
|
+
selection: parseWorkSessionSelection(input.selection),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function parseWorkSessionCompletion(value) {
|
|
139
|
+
const status = value?.status;
|
|
140
|
+
if (status === 'attached') {
|
|
141
|
+
const input = object(value, ['status', 'nativeSessionId', 'settings'], 'attached result');
|
|
142
|
+
return { status, nativeSessionId: workSessionId(input.nativeSessionId), settings: parseWorkSessionSettings(input.settings) };
|
|
143
|
+
}
|
|
144
|
+
if (status === 'failed' || status === 'uncertain') {
|
|
145
|
+
const input = object(value, ['status', 'error'], 'failure result');
|
|
146
|
+
const error = object(input.error, ['code', 'message'], 'failure');
|
|
147
|
+
return { status, error: { code: workSessionId(error.code), message: label(error.message, 'failure message', 512) } };
|
|
148
|
+
}
|
|
149
|
+
throw new Error('Invalid work session result');
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/backend-contracts",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.5.0",
|
|
4
4
|
"description": "Canon backend contract helpers shared by Functions and stream-service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -33,21 +33,19 @@
|
|
|
33
33
|
"contracts",
|
|
34
34
|
"wire"
|
|
35
35
|
],
|
|
36
|
-
"
|
|
37
|
-
"type": "git",
|
|
38
|
-
"url": "https://github.com/HeyBobChan/canon",
|
|
39
|
-
"directory": "packages/backend-contracts"
|
|
40
|
-
},
|
|
41
|
-
"homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/backend-contracts",
|
|
36
|
+
"homepage": "https://canonmail.com/agents/contracts",
|
|
42
37
|
"publishConfig": {
|
|
43
38
|
"access": "public"
|
|
44
39
|
},
|
|
45
40
|
"devDependencies": {
|
|
46
|
-
"@canonmsg/rich-cards": "^0.10.
|
|
41
|
+
"@canonmsg/rich-cards": "^0.10.5",
|
|
47
42
|
"@types/node": "^22.0.0",
|
|
48
43
|
"ajv": "^8.20.0",
|
|
49
44
|
"typescript": "~5.7.0",
|
|
50
45
|
"vitest": "^4.1.8"
|
|
51
46
|
},
|
|
52
|
-
"license": "MIT"
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://canonmail.com/support"
|
|
50
|
+
}
|
|
53
51
|
}
|