@canonmsg/core 0.5.0 → 0.7.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/base-url.d.ts +1 -0
- package/dist/base-url.js +9 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +1 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +48 -0
- package/dist/execution-environment.d.ts +2 -2
- package/dist/execution-environment.js +18 -23
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/rtdb-rest.d.ts +20 -2
- package/dist/rtdb-rest.js +175 -130
- package/dist/types.d.ts +6 -0
- package/dist/work-session.d.ts +88 -0
- package/dist/work-session.js +238 -0
- package/package.json +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveCanonBaseUrl(input?: string | null): string;
|
package/dist/base-url.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DEFAULT_BASE_URL } from './constants.js';
|
|
2
|
+
export function resolveCanonBaseUrl(input) {
|
|
3
|
+
if (typeof input !== 'string')
|
|
4
|
+
return DEFAULT_BASE_URL;
|
|
5
|
+
const trimmed = input.trim();
|
|
6
|
+
if (!trimmed)
|
|
7
|
+
return DEFAULT_BASE_URL;
|
|
8
|
+
return trimmed.replace(/\/+$/, '');
|
|
9
|
+
}
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
2
|
export type { AgentCapabilities, AgentClientType, AgentRuntime, MediaAttachment, MediaAttachmentKind, ModelOption, SessionConfig, WorkspaceOption, } from './types.js';
|
|
3
|
+
export type { CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions, WorkSessionPromptRenderOptions, } from './work-session.js';
|
|
4
|
+
export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
|
|
3
5
|
export type { AgentBehaviorSettings, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationStyle, ResolvedAgentBehaviorPolicy, } from './policy.js';
|
|
4
6
|
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
5
7
|
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
package/dist/browser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
+
export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
|
|
2
3
|
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
3
4
|
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CanonMessage, type CanonConversation, type CanonMessagesPage, type AgentContext, type MediaAttachment, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
|
|
2
|
+
import type { CanonResolvedWorkSession, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions } from './work-session.js';
|
|
2
3
|
import type { InboundDisposition } from './turn-protocol.js';
|
|
3
4
|
/**
|
|
4
5
|
* Thin REST client for Canon's agent API.
|
|
@@ -21,6 +22,10 @@ export declare class CanonClient {
|
|
|
21
22
|
sendMessage(conversationId: string, text: string, options?: SendMessageOptions): Promise<{
|
|
22
23
|
messageId: string;
|
|
23
24
|
}>;
|
|
25
|
+
createWorkSession(options: CreateWorkSessionOptions): Promise<CanonResolvedWorkSession>;
|
|
26
|
+
getWorkSession(workSessionId: string, conversationId: string): Promise<CanonResolvedWorkSession>;
|
|
27
|
+
upsertWorkSessionConversation(workSessionId: string, conversationId: string, options?: UpdateWorkSessionConversationOptions): Promise<CanonResolvedWorkSession>;
|
|
28
|
+
sendLinkedMessage(options: SendLinkedMessageOptions): Promise<SendLinkedMessageResult>;
|
|
24
29
|
createConversation(options: CreateConversationOptions): Promise<{
|
|
25
30
|
conversationId: string;
|
|
26
31
|
}>;
|
package/dist/client.js
CHANGED
|
@@ -73,6 +73,54 @@ export class CanonClient {
|
|
|
73
73
|
throw new CanonApiError(res.status, await res.text());
|
|
74
74
|
return res.json();
|
|
75
75
|
}
|
|
76
|
+
async createWorkSession(options) {
|
|
77
|
+
const res = await fetch(`${this.baseUrl}/work-sessions`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: this.authHeaders(),
|
|
80
|
+
body: JSON.stringify(options),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok)
|
|
83
|
+
throw new CanonApiError(res.status, await res.text());
|
|
84
|
+
return res.json();
|
|
85
|
+
}
|
|
86
|
+
async getWorkSession(workSessionId, conversationId) {
|
|
87
|
+
const params = new URLSearchParams({ conversationId });
|
|
88
|
+
const res = await fetch(`${this.baseUrl}/work-sessions/${workSessionId}?${params}`, { headers: this.authHeaders() });
|
|
89
|
+
if (!res.ok)
|
|
90
|
+
throw new CanonApiError(res.status, await res.text());
|
|
91
|
+
return res.json();
|
|
92
|
+
}
|
|
93
|
+
async upsertWorkSessionConversation(workSessionId, conversationId, options) {
|
|
94
|
+
const res = await fetch(`${this.baseUrl}/work-sessions/${workSessionId}/conversations/${conversationId}`, {
|
|
95
|
+
method: 'PUT',
|
|
96
|
+
headers: this.authHeaders(),
|
|
97
|
+
body: JSON.stringify(options ?? {}),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new CanonApiError(res.status, await res.text());
|
|
101
|
+
return res.json();
|
|
102
|
+
}
|
|
103
|
+
async sendLinkedMessage(options) {
|
|
104
|
+
const res = await fetch(`${this.baseUrl}/messages/send-linked`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: this.authHeaders(),
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
sourceConversationId: options.sourceConversationId,
|
|
109
|
+
targetConversationId: options.targetConversationId,
|
|
110
|
+
text: options.text,
|
|
111
|
+
...(options.workSessionId ? { workSessionId: options.workSessionId } : {}),
|
|
112
|
+
...(options.createWorkSession
|
|
113
|
+
? { createWorkSession: options.createWorkSession }
|
|
114
|
+
: {}),
|
|
115
|
+
...(options.sourceContext ? { sourceContext: options.sourceContext } : {}),
|
|
116
|
+
...(options.targetContext ? { targetContext: options.targetContext } : {}),
|
|
117
|
+
...(options.messageOptions ? { messageOptions: options.messageOptions } : {}),
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok)
|
|
121
|
+
throw new CanonApiError(res.status, await res.text());
|
|
122
|
+
return res.json();
|
|
123
|
+
}
|
|
76
124
|
async createConversation(options) {
|
|
77
125
|
const res = await fetch(`${this.baseUrl}/conversations/create`, {
|
|
78
126
|
method: 'POST',
|
|
@@ -23,8 +23,8 @@ export interface ConfiguredWorkspaceOption extends WorkspaceResolverOption {
|
|
|
23
23
|
}
|
|
24
24
|
export interface SessionWorkspaceConfig {
|
|
25
25
|
workspaceId?: string;
|
|
26
|
-
legacyCwd?: string;
|
|
27
26
|
model?: string;
|
|
27
|
+
retiredWorkspaceConfig?: boolean;
|
|
28
28
|
}
|
|
29
29
|
export declare function normalizeOptionalString(value: unknown): string | undefined;
|
|
30
30
|
export declare function isEnabledFlag(value: unknown): boolean;
|
|
@@ -37,7 +37,7 @@ export declare function resolveConfiguredWorkspaceCwd(input: {
|
|
|
37
37
|
workspaceOptions: WorkspaceResolverOption[];
|
|
38
38
|
config: {
|
|
39
39
|
workspaceId?: string;
|
|
40
|
-
|
|
40
|
+
retiredWorkspaceConfig?: boolean;
|
|
41
41
|
} | null;
|
|
42
42
|
defaultCwd: string;
|
|
43
43
|
}): string;
|
|
@@ -153,48 +153,43 @@ export function buildConfiguredWorkspaceOptions(primaryCwd, configured) {
|
|
|
153
153
|
export function buildPublicWorkspaceOptions(workspaceOptions) {
|
|
154
154
|
return workspaceOptions.map(({ id, label }) => ({ id, label }));
|
|
155
155
|
}
|
|
156
|
+
function isRetiredWorkspaceId(value) {
|
|
157
|
+
if (!value)
|
|
158
|
+
return false;
|
|
159
|
+
return value === 'default' || /^workspace-\d+$/.test(value);
|
|
160
|
+
}
|
|
156
161
|
export function readSessionWorkspaceConfig(raw) {
|
|
157
162
|
if (!raw || typeof raw !== 'object')
|
|
158
163
|
return null;
|
|
159
164
|
const data = raw;
|
|
165
|
+
const rawWorkspaceId = normalizeOptionalString(data.workspaceId);
|
|
166
|
+
const stableWorkspaceId = rawWorkspaceId && !isRetiredWorkspaceId(rawWorkspaceId)
|
|
167
|
+
? rawWorkspaceId
|
|
168
|
+
: undefined;
|
|
169
|
+
const hasRetiredWorkspaceReference = (normalizeOptionalString(data.cwd) !== undefined
|
|
170
|
+
|| isRetiredWorkspaceId(rawWorkspaceId));
|
|
160
171
|
return {
|
|
161
|
-
workspaceId:
|
|
162
|
-
legacyCwd: normalizeOptionalString(data.cwd),
|
|
172
|
+
workspaceId: stableWorkspaceId,
|
|
163
173
|
model: normalizeOptionalString(data.model),
|
|
174
|
+
...(!stableWorkspaceId && hasRetiredWorkspaceReference
|
|
175
|
+
? { retiredWorkspaceConfig: true }
|
|
176
|
+
: {}),
|
|
164
177
|
};
|
|
165
178
|
}
|
|
166
|
-
function findWorkspaceByLegacyCwd(workspaceOptions, legacyCwd) {
|
|
167
|
-
if (!legacyCwd)
|
|
168
|
-
return undefined;
|
|
169
|
-
const resolvedLegacyCwd = resolve(legacyCwd);
|
|
170
|
-
return workspaceOptions.find((workspace) => resolve(workspace.cwd) === resolvedLegacyCwd);
|
|
171
|
-
}
|
|
172
179
|
export function resolveConfiguredWorkspaceCwd(input) {
|
|
173
180
|
const fallbackCwd = input.workspaceOptions[0]?.cwd ?? resolve(input.defaultCwd);
|
|
174
181
|
if (!input.config)
|
|
175
182
|
return fallbackCwd;
|
|
176
|
-
|
|
183
|
+
if (input.config.retiredWorkspaceConfig) {
|
|
184
|
+
throw new ExecutionEnvironmentError('Session config still references a retired workspace format.', 'This Canon coding session was saved with a retired workspace format. Recreate the session or select a current workspace.');
|
|
185
|
+
}
|
|
177
186
|
const workspaceId = input.config.workspaceId;
|
|
178
187
|
if (workspaceId) {
|
|
179
188
|
const workspace = input.workspaceOptions.find((option) => option.id === workspaceId);
|
|
180
189
|
if (workspace)
|
|
181
190
|
return workspace.cwd;
|
|
182
|
-
if (workspaceId === 'default') {
|
|
183
|
-
return fallbackCwd;
|
|
184
|
-
}
|
|
185
|
-
const legacyWorkspaceMatch = /^workspace-(\d+)$/.exec(workspaceId);
|
|
186
|
-
if (legacyWorkspaceMatch) {
|
|
187
|
-
const legacyIndex = Number.parseInt(legacyWorkspaceMatch[1] ?? '', 10) - 1;
|
|
188
|
-
if (legacyIndex >= 0 && input.workspaceOptions[legacyIndex]) {
|
|
189
|
-
return input.workspaceOptions[legacyIndex].cwd;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (legacyWorkspace)
|
|
193
|
-
return legacyWorkspace.cwd;
|
|
194
191
|
throw new ExecutionEnvironmentError(`Workspace ${workspaceId} is not configured on this machine.`, 'The workspace saved for this Canon coding session is no longer configured on this machine.');
|
|
195
192
|
}
|
|
196
|
-
if (legacyWorkspace)
|
|
197
|
-
return legacyWorkspace.cwd;
|
|
198
193
|
return fallbackCwd;
|
|
199
194
|
}
|
|
200
195
|
export function buildConversationWorktreeSpec(input) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
2
|
export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, CanonMessagesPage, AgentContext, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
|
|
3
|
+
export type { CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CreateWorkSessionOptions, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions, WorkSessionPromptRenderOptions, } from './work-session.js';
|
|
4
|
+
export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
|
|
3
5
|
export { CanonClient, CanonApiError } from './client.js';
|
|
4
6
|
export { CanonStream } from './stream.js';
|
|
5
7
|
export type { StreamHandler } from './stream.js';
|
|
@@ -23,3 +25,4 @@ export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecu
|
|
|
23
25
|
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
24
26
|
export type { SessionStatePayload, TurnStatePayload } from './rtdb-rest.js';
|
|
25
27
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
|
28
|
+
export { resolveCanonBaseUrl } from './base-url.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Types
|
|
2
2
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
3
|
+
export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
|
|
3
4
|
// Client
|
|
4
5
|
export { CanonClient, CanonApiError } from './client.js';
|
|
5
6
|
// Stream
|
|
@@ -25,3 +26,5 @@ export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, build
|
|
|
25
26
|
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
26
27
|
// Constants
|
|
27
28
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
|
29
|
+
// Base URL resolver
|
|
30
|
+
export { resolveCanonBaseUrl } from './base-url.js';
|
package/dist/rtdb-rest.d.ts
CHANGED
|
@@ -50,8 +50,25 @@ export interface TurnStatePayload {
|
|
|
50
50
|
'.sv': 'timestamp';
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
interface RTDBAuthOptions {
|
|
54
|
+
rtdbUrl?: string;
|
|
55
|
+
firebaseApiKey?: string;
|
|
56
|
+
}
|
|
57
|
+
interface RTDBClientHandle {
|
|
58
|
+
read(path: string): Promise<unknown>;
|
|
59
|
+
write(path: string, data: unknown): Promise<void>;
|
|
60
|
+
patch(path: string, data: unknown): Promise<void>;
|
|
61
|
+
remove(path: string): Promise<void>;
|
|
62
|
+
writeSessionState(conversationId: string, agentId: string, state: Omit<SessionStatePayload, 'updatedAt'>): Promise<void>;
|
|
63
|
+
clearSessionState(conversationId: string, agentId: string): Promise<void>;
|
|
64
|
+
writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
|
|
65
|
+
clearTurnState(conversationId: string, agentId: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Initializes the default RTDB helper and returns a scoped client for callers
|
|
69
|
+
* that need per-runtime auth and base-url isolation.
|
|
70
|
+
*/
|
|
71
|
+
export declare function initRTDBAuth(client: CanonClient, options?: RTDBAuthOptions): RTDBClientHandle;
|
|
55
72
|
/** Generic RTDB REST write (PUT). */
|
|
56
73
|
export declare function rtdbWrite(path: string, data: unknown): Promise<void>;
|
|
57
74
|
/** Generic RTDB REST read (GET). */
|
|
@@ -67,3 +84,4 @@ export declare function writeSessionState(conversationId: string, agentId: strin
|
|
|
67
84
|
export declare function clearSessionState(conversationId: string, agentId: string): Promise<void>;
|
|
68
85
|
export declare function writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
|
|
69
86
|
export declare function clearTurnState(conversationId: string, agentId: string): Promise<void>;
|
|
87
|
+
export {};
|
package/dist/rtdb-rest.js
CHANGED
|
@@ -6,163 +6,208 @@
|
|
|
6
6
|
* is exchanged for a Firebase ID token before use with RTDB REST.
|
|
7
7
|
*/
|
|
8
8
|
import { DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let tokenClient = null;
|
|
15
|
-
/** Must be called once before any RTDB operations. */
|
|
16
|
-
export function initRTDBAuth(client) {
|
|
17
|
-
tokenClient = client;
|
|
9
|
+
const DEFAULT_RTDB_BASE = normalizeRTDBBase(process.env.CANON_RTDB_URL || DEFAULT_RTDB_URL);
|
|
10
|
+
const DEFAULT_FIREBASE_API_KEY = process.env.CANON_FIREBASE_API_KEY || FIREBASE_WEB_API_KEY;
|
|
11
|
+
let defaultRTDBClient = null;
|
|
12
|
+
function normalizeRTDBBase(url) {
|
|
13
|
+
return url.replace(/\/+$/, '');
|
|
18
14
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
async function exchangeCustomTokenForIdToken(customToken) {
|
|
23
|
-
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
|
|
24
|
-
const res = await fetch(url, {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: { 'Content-Type': 'application/json' },
|
|
27
|
-
body: JSON.stringify({ token: customToken, returnSecureToken: true }),
|
|
28
|
-
});
|
|
29
|
-
if (!res.ok) {
|
|
30
|
-
const text = await res.text();
|
|
31
|
-
throw new Error(`Token exchange failed (${res.status}): ${text}`);
|
|
32
|
-
}
|
|
33
|
-
const data = await res.json();
|
|
34
|
-
return { idToken: data.idToken, expiresIn: parseInt(data.expiresIn, 10) };
|
|
15
|
+
function normalizeRTDBPath(path) {
|
|
16
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
35
17
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
18
|
+
function createRTDBClientHandle(client, options) {
|
|
19
|
+
const rtdbBase = normalizeRTDBBase(options?.rtdbUrl || DEFAULT_RTDB_BASE);
|
|
20
|
+
const firebaseApiKey = options?.firebaseApiKey || DEFAULT_FIREBASE_API_KEY;
|
|
21
|
+
let cachedIdToken = null;
|
|
22
|
+
let idTokenExpiresAt = 0;
|
|
23
|
+
let refreshPromise = null;
|
|
24
|
+
async function exchangeCustomTokenForIdToken(customToken) {
|
|
25
|
+
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${firebaseApiKey}`;
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ token: customToken, returnSecureToken: true }),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const text = await res.text();
|
|
33
|
+
throw new Error(`Token exchange failed (${res.status}): ${text}`);
|
|
34
|
+
}
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
return {
|
|
37
|
+
idToken: data.idToken,
|
|
38
|
+
expiresIn: Number.parseInt(data.expiresIn, 10),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function getToken(forceRefresh = false) {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (!forceRefresh && cachedIdToken && now <= idTokenExpiresAt - 5 * 60 * 1000) {
|
|
44
|
+
return cachedIdToken;
|
|
45
|
+
}
|
|
46
|
+
if (refreshPromise) {
|
|
47
|
+
return refreshPromise;
|
|
48
|
+
}
|
|
49
|
+
const staleToken = forceRefresh ? null : cachedIdToken;
|
|
50
|
+
refreshPromise = (async () => {
|
|
51
|
+
try {
|
|
52
|
+
const auth = await client.getAuthToken();
|
|
53
|
+
const { idToken, expiresIn } = await exchangeCustomTokenForIdToken(auth.token);
|
|
54
|
+
cachedIdToken = idToken;
|
|
55
|
+
idTokenExpiresAt = Date.now() + (expiresIn * 1000);
|
|
56
|
+
return cachedIdToken;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error('[canon] RTDB token refresh failed:', err);
|
|
60
|
+
return staleToken;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
refreshPromise = null;
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
return refreshPromise;
|
|
67
|
+
}
|
|
68
|
+
async function request(method, path, data, options = {}) {
|
|
69
|
+
const token = await getToken(false);
|
|
70
|
+
if (!token)
|
|
41
71
|
return null;
|
|
72
|
+
const url = `${rtdbBase}${normalizeRTDBPath(path)}.json?auth=${encodeURIComponent(token)}`;
|
|
73
|
+
const res = await fetch(url, {
|
|
74
|
+
method,
|
|
75
|
+
...(method === 'GET'
|
|
76
|
+
? {}
|
|
77
|
+
: {
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
...(data === undefined ? {} : { body: JSON.stringify(data) }),
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
if (options.retryUnauthorized !== false
|
|
83
|
+
&& (res.status === 401 || res.status === 403)) {
|
|
84
|
+
cachedIdToken = null;
|
|
85
|
+
idTokenExpiresAt = 0;
|
|
86
|
+
const refreshedToken = await getToken(true);
|
|
87
|
+
if (!refreshedToken) {
|
|
88
|
+
return res;
|
|
89
|
+
}
|
|
90
|
+
const retryUrl = `${rtdbBase}${normalizeRTDBPath(path)}.json?auth=${encodeURIComponent(refreshedToken)}`;
|
|
91
|
+
return fetch(retryUrl, {
|
|
92
|
+
method,
|
|
93
|
+
...(method === 'GET'
|
|
94
|
+
? {}
|
|
95
|
+
: {
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
...(data === undefined ? {} : { body: JSON.stringify(data) }),
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return res;
|
|
102
|
+
}
|
|
103
|
+
async function requireSuccess(method, path, data, errorPrefix) {
|
|
104
|
+
const res = await request(method, path, data);
|
|
105
|
+
if (!res)
|
|
106
|
+
return;
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const text = await res.text();
|
|
109
|
+
throw new Error(`${errorPrefix} (${res.status}): ${text}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function read(path) {
|
|
113
|
+
const res = await request('GET', path);
|
|
114
|
+
if (!res?.ok)
|
|
115
|
+
return null;
|
|
116
|
+
return res.json();
|
|
117
|
+
}
|
|
118
|
+
async function write(path, data) {
|
|
119
|
+
await requireSuccess('PUT', path, data, 'RTDB write failed');
|
|
120
|
+
}
|
|
121
|
+
async function patch(path, data) {
|
|
122
|
+
await requireSuccess('PATCH', path, data, 'RTDB patch failed');
|
|
123
|
+
}
|
|
124
|
+
async function remove(path) {
|
|
125
|
+
await requireSuccess('DELETE', path, undefined, 'RTDB delete failed');
|
|
126
|
+
}
|
|
127
|
+
async function writeSessionStateImpl(conversationId, agentId, state) {
|
|
128
|
+
await write(`/session-state/${conversationId}/${agentId}`, {
|
|
129
|
+
...state,
|
|
130
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function clearSessionStateImpl(conversationId, agentId) {
|
|
42
134
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
135
|
+
await write(`/session-state/${conversationId}/${agentId}`, {
|
|
136
|
+
isActive: false,
|
|
137
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
138
|
+
});
|
|
47
139
|
}
|
|
48
|
-
catch (
|
|
49
|
-
console.error('[canon] RTDB
|
|
50
|
-
return cachedIdToken; // Return stale token as fallback
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error('[canon] RTDB clear failed:', error);
|
|
51
142
|
}
|
|
52
143
|
}
|
|
53
|
-
|
|
144
|
+
async function writeTurnStateImpl(conversationId, agentId, state) {
|
|
145
|
+
await write(`/turn-state/${conversationId}/${agentId}`, {
|
|
146
|
+
...state,
|
|
147
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async function clearTurnStateImpl(conversationId, agentId) {
|
|
151
|
+
try {
|
|
152
|
+
await write(`/turn-state/${conversationId}/${agentId}`, {
|
|
153
|
+
state: 'idle',
|
|
154
|
+
queueDepth: 0,
|
|
155
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
156
|
+
completedAt: { '.sv': 'timestamp' },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error('[canon] RTDB turn clear failed:', error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
read,
|
|
165
|
+
write,
|
|
166
|
+
patch,
|
|
167
|
+
remove,
|
|
168
|
+
writeSessionState: writeSessionStateImpl,
|
|
169
|
+
clearSessionState: clearSessionStateImpl,
|
|
170
|
+
writeTurnState: writeTurnStateImpl,
|
|
171
|
+
clearTurnState: clearTurnStateImpl,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Initializes the default RTDB helper and returns a scoped client for callers
|
|
176
|
+
* that need per-runtime auth and base-url isolation.
|
|
177
|
+
*/
|
|
178
|
+
export function initRTDBAuth(client, options) {
|
|
179
|
+
const scopedClient = createRTDBClientHandle(client, options);
|
|
180
|
+
defaultRTDBClient = scopedClient;
|
|
181
|
+
return scopedClient;
|
|
182
|
+
}
|
|
183
|
+
function getDefaultRTDBClient() {
|
|
184
|
+
return defaultRTDBClient;
|
|
54
185
|
}
|
|
55
186
|
// ── RTDB operations ───────────────────────────────────────────────────
|
|
56
187
|
/** Generic RTDB REST write (PUT). */
|
|
57
188
|
export async function rtdbWrite(path, data) {
|
|
58
|
-
|
|
59
|
-
if (!token)
|
|
60
|
-
return;
|
|
61
|
-
const url = `${RTDB_BASE}${path}.json?auth=${token}`;
|
|
62
|
-
const res = await fetch(url, {
|
|
63
|
-
method: 'PUT',
|
|
64
|
-
headers: { 'Content-Type': 'application/json' },
|
|
65
|
-
body: JSON.stringify(data),
|
|
66
|
-
});
|
|
67
|
-
if (!res.ok) {
|
|
68
|
-
const text = await res.text();
|
|
69
|
-
throw new Error(`RTDB write failed (${res.status}): ${text}`);
|
|
70
|
-
}
|
|
189
|
+
await getDefaultRTDBClient()?.write(path, data);
|
|
71
190
|
}
|
|
72
191
|
/** Generic RTDB REST read (GET). */
|
|
73
192
|
export async function rtdbRead(path) {
|
|
74
|
-
|
|
75
|
-
if (!token)
|
|
76
|
-
return null;
|
|
77
|
-
const url = `${RTDB_BASE}${path}.json?auth=${token}`;
|
|
78
|
-
const res = await fetch(url);
|
|
79
|
-
if (!res.ok)
|
|
80
|
-
return null;
|
|
81
|
-
return res.json();
|
|
193
|
+
return getDefaultRTDBClient()?.read(path) ?? null;
|
|
82
194
|
}
|
|
83
195
|
/**
|
|
84
196
|
* Write session state to RTDB via REST API.
|
|
85
197
|
* Path: /session-state/{conversationId}/{agentId}
|
|
86
198
|
*/
|
|
87
199
|
export async function writeSessionState(conversationId, agentId, state) {
|
|
88
|
-
|
|
89
|
-
if (!token)
|
|
90
|
-
return;
|
|
91
|
-
const url = `${RTDB_BASE}/session-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
92
|
-
const body = {
|
|
93
|
-
...state,
|
|
94
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
95
|
-
};
|
|
96
|
-
const res = await fetch(url, {
|
|
97
|
-
method: 'PUT',
|
|
98
|
-
headers: { 'Content-Type': 'application/json' },
|
|
99
|
-
body: JSON.stringify(body),
|
|
100
|
-
});
|
|
101
|
-
if (!res.ok) {
|
|
102
|
-
const text = await res.text();
|
|
103
|
-
throw new Error(`RTDB write failed (${res.status}): ${text}`);
|
|
104
|
-
}
|
|
200
|
+
await getDefaultRTDBClient()?.writeSessionState(conversationId, agentId, state);
|
|
105
201
|
}
|
|
106
202
|
/**
|
|
107
203
|
* Clear session state from RTDB (full overwrite with isActive: false).
|
|
108
204
|
*/
|
|
109
205
|
export async function clearSessionState(conversationId, agentId) {
|
|
110
|
-
|
|
111
|
-
if (!token)
|
|
112
|
-
return;
|
|
113
|
-
const url = `${RTDB_BASE}/session-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
114
|
-
const body = {
|
|
115
|
-
isActive: false,
|
|
116
|
-
updatedAt: { '.sv': 'timestamp' },
|
|
117
|
-
};
|
|
118
|
-
// Use PUT (not PATCH) to clear stale fields like model/cwd
|
|
119
|
-
const res = await fetch(url, {
|
|
120
|
-
method: 'PUT',
|
|
121
|
-
headers: { 'Content-Type': 'application/json' },
|
|
122
|
-
body: JSON.stringify(body),
|
|
123
|
-
});
|
|
124
|
-
if (!res.ok) {
|
|
125
|
-
const text = await res.text();
|
|
126
|
-
console.error(`[canon] RTDB clear failed (${res.status}): ${text}`);
|
|
127
|
-
}
|
|
206
|
+
await getDefaultRTDBClient()?.clearSessionState(conversationId, agentId);
|
|
128
207
|
}
|
|
129
208
|
export async function writeTurnState(conversationId, agentId, state) {
|
|
130
|
-
|
|
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
|
-
}
|
|
209
|
+
await getDefaultRTDBClient()?.writeTurnState(conversationId, agentId, state);
|
|
147
210
|
}
|
|
148
211
|
export async function clearTurnState(conversationId, agentId) {
|
|
149
|
-
|
|
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
|
-
}
|
|
212
|
+
await getDefaultRTDBClient()?.clearTurnState(conversationId, agentId);
|
|
168
213
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedAgentBehaviorPolicy } from './policy.js';
|
|
2
|
+
import type { CanonWorkSessionContext } from './work-session.js';
|
|
2
3
|
export type MediaAttachmentKind = 'image' | 'audio' | 'file';
|
|
3
4
|
export interface MediaAttachment {
|
|
4
5
|
kind: MediaAttachmentKind;
|
|
@@ -25,6 +26,7 @@ export interface CanonMessage {
|
|
|
25
26
|
mentions: string[];
|
|
26
27
|
replyTo: string | null;
|
|
27
28
|
replyToPosition: number | null;
|
|
29
|
+
workSession?: CanonWorkSessionContext | null;
|
|
28
30
|
metadata?: Record<string, unknown>;
|
|
29
31
|
status: 'sent' | 'read';
|
|
30
32
|
deleted: boolean;
|
|
@@ -50,6 +52,7 @@ export interface CanonConversation {
|
|
|
50
52
|
export interface CanonMessagesPage {
|
|
51
53
|
messages: CanonMessage[];
|
|
52
54
|
behavior?: ResolvedAgentBehaviorPolicy;
|
|
55
|
+
workSessions?: CanonWorkSessionContext[];
|
|
53
56
|
}
|
|
54
57
|
export type AgentClientType = 'claude-code' | 'openclaw' | 'codex' | 'generic';
|
|
55
58
|
/** Declares what session controls an agent type supports. */
|
|
@@ -88,6 +91,7 @@ export interface AgentContext {
|
|
|
88
91
|
export interface MessageCreatedPayload {
|
|
89
92
|
conversationId: string;
|
|
90
93
|
behavior?: ResolvedAgentBehaviorPolicy;
|
|
94
|
+
workSessions?: CanonWorkSessionContext[];
|
|
91
95
|
message: {
|
|
92
96
|
id: string;
|
|
93
97
|
senderId: string;
|
|
@@ -105,6 +109,7 @@ export interface MessageCreatedPayload {
|
|
|
105
109
|
replyToPosition?: number;
|
|
106
110
|
mentions?: string[];
|
|
107
111
|
createdAt?: string;
|
|
112
|
+
workSession?: CanonWorkSessionContext | null;
|
|
108
113
|
/** Structured metadata for rich UI (approval cards, etc.) */
|
|
109
114
|
metadata?: Record<string, unknown>;
|
|
110
115
|
};
|
|
@@ -129,6 +134,7 @@ export interface SendMessageOptions {
|
|
|
129
134
|
attachments?: MediaAttachment[];
|
|
130
135
|
contactCardUserId?: string;
|
|
131
136
|
mentions?: string[];
|
|
137
|
+
workSessionId?: string;
|
|
132
138
|
/** Structured metadata for rich UI (approval cards, etc.) */
|
|
133
139
|
metadata?: Record<string, unknown>;
|
|
134
140
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { RepresentationMode } from './policy.js';
|
|
2
|
+
import type { SendMessageOptions } from './types.js';
|
|
3
|
+
export type CanonWorkSessionStatus = 'active' | 'paused' | 'completed';
|
|
4
|
+
export type CanonWorkSessionConversationRole = 'requester' | 'coordinator' | 'participant' | 'delegate' | 'observer';
|
|
5
|
+
export type CanonWorkSessionDisclosureMode = 'none' | 'summary' | 'full';
|
|
6
|
+
export interface CanonWorkSessionParticipant {
|
|
7
|
+
conversationId?: string | null;
|
|
8
|
+
participantId?: string | null;
|
|
9
|
+
label?: string | null;
|
|
10
|
+
role?: CanonWorkSessionConversationRole | null;
|
|
11
|
+
}
|
|
12
|
+
export interface CanonWorkSessionContext {
|
|
13
|
+
id: string;
|
|
14
|
+
title?: string | null;
|
|
15
|
+
objective?: string | null;
|
|
16
|
+
status?: CanonWorkSessionStatus | null;
|
|
17
|
+
summary?: string | null;
|
|
18
|
+
representation?: RepresentationMode | null;
|
|
19
|
+
currentConversationRole?: CanonWorkSessionConversationRole | null;
|
|
20
|
+
sourceConversationId?: string | null;
|
|
21
|
+
sourceConversationLabel?: string | null;
|
|
22
|
+
sourceParticipantId?: string | null;
|
|
23
|
+
sourceParticipantLabel?: string | null;
|
|
24
|
+
disclosure?: CanonWorkSessionDisclosureMode | null;
|
|
25
|
+
disclosureNotes?: string[];
|
|
26
|
+
visibleFacts?: string[];
|
|
27
|
+
pendingQuestions?: string[];
|
|
28
|
+
participants?: CanonWorkSessionParticipant[];
|
|
29
|
+
updatedAt?: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface CanonWorkSession {
|
|
32
|
+
id: string;
|
|
33
|
+
title?: string | null;
|
|
34
|
+
objective?: string | null;
|
|
35
|
+
status: CanonWorkSessionStatus;
|
|
36
|
+
createdAt?: string | null;
|
|
37
|
+
updatedAt?: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface UpdateWorkSessionConversationOptions {
|
|
40
|
+
summary?: string | null;
|
|
41
|
+
representation?: RepresentationMode | null;
|
|
42
|
+
currentConversationRole?: CanonWorkSessionConversationRole | null;
|
|
43
|
+
sourceConversationId?: string | null;
|
|
44
|
+
sourceConversationLabel?: string | null;
|
|
45
|
+
sourceParticipantId?: string | null;
|
|
46
|
+
sourceParticipantLabel?: string | null;
|
|
47
|
+
disclosure?: CanonWorkSessionDisclosureMode | null;
|
|
48
|
+
disclosureNotes?: string[];
|
|
49
|
+
visibleFacts?: string[];
|
|
50
|
+
pendingQuestions?: string[];
|
|
51
|
+
participants?: CanonWorkSessionParticipant[];
|
|
52
|
+
}
|
|
53
|
+
export interface CreateWorkSessionOptions extends UpdateWorkSessionConversationOptions {
|
|
54
|
+
conversationId: string;
|
|
55
|
+
title?: string | null;
|
|
56
|
+
objective?: string | null;
|
|
57
|
+
status?: CanonWorkSessionStatus | null;
|
|
58
|
+
}
|
|
59
|
+
export interface CanonResolvedWorkSession {
|
|
60
|
+
workSession: CanonWorkSession;
|
|
61
|
+
context: CanonWorkSessionContext;
|
|
62
|
+
}
|
|
63
|
+
export interface SendLinkedMessageOptions {
|
|
64
|
+
sourceConversationId: string;
|
|
65
|
+
targetConversationId: string;
|
|
66
|
+
text: string;
|
|
67
|
+
workSessionId?: string;
|
|
68
|
+
createWorkSession?: Omit<CreateWorkSessionOptions, 'conversationId'>;
|
|
69
|
+
sourceContext?: UpdateWorkSessionConversationOptions;
|
|
70
|
+
targetContext?: UpdateWorkSessionConversationOptions;
|
|
71
|
+
messageOptions?: Omit<SendMessageOptions, 'workSessionId'>;
|
|
72
|
+
}
|
|
73
|
+
export interface SendLinkedMessageResult {
|
|
74
|
+
messageId: string;
|
|
75
|
+
workSessionId: string;
|
|
76
|
+
workSessionCreated: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface WorkSessionPromptRenderOptions {
|
|
79
|
+
maxVisibleFacts?: number;
|
|
80
|
+
maxPendingQuestions?: number;
|
|
81
|
+
maxDisclosureNotes?: number;
|
|
82
|
+
maxParticipants?: number;
|
|
83
|
+
maxDetailedWorkSessions?: number;
|
|
84
|
+
maxOtherWorkSessions?: number;
|
|
85
|
+
}
|
|
86
|
+
export declare function mergeWorkSessionContexts(explicitWorkSession?: CanonWorkSessionContext | null, activeWorkSessions?: CanonWorkSessionContext[] | null): CanonWorkSessionContext[];
|
|
87
|
+
export declare function buildWorkSessionPromptLines(workSession?: CanonWorkSessionContext | null, options?: WorkSessionPromptRenderOptions): string[];
|
|
88
|
+
export declare function buildWorkSessionsPromptLines(workSessions?: CanonWorkSessionContext[] | null, options?: WorkSessionPromptRenderOptions): string[];
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const DEFAULT_WORK_SESSION_PROMPT_OPTIONS = {
|
|
2
|
+
maxVisibleFacts: 3,
|
|
3
|
+
maxPendingQuestions: 2,
|
|
4
|
+
maxDisclosureNotes: 1,
|
|
5
|
+
maxParticipants: 3,
|
|
6
|
+
maxDetailedWorkSessions: 1,
|
|
7
|
+
maxOtherWorkSessions: 3,
|
|
8
|
+
};
|
|
9
|
+
function normalizeString(value) {
|
|
10
|
+
if (typeof value !== 'string')
|
|
11
|
+
return null;
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
14
|
+
}
|
|
15
|
+
function normalizeStringList(value) {
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
return [];
|
|
18
|
+
return value
|
|
19
|
+
.map((entry) => normalizeString(entry))
|
|
20
|
+
.filter((entry) => entry != null);
|
|
21
|
+
}
|
|
22
|
+
function mergeStringLists(primary, secondary) {
|
|
23
|
+
const merged = [...normalizeStringList(primary), ...normalizeStringList(secondary)];
|
|
24
|
+
const unique = Array.from(new Set(merged));
|
|
25
|
+
return unique.length > 0 ? unique : undefined;
|
|
26
|
+
}
|
|
27
|
+
function mergeParticipants(primary, secondary) {
|
|
28
|
+
const result = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const pushParticipant = (participant) => {
|
|
31
|
+
if (!participant)
|
|
32
|
+
return;
|
|
33
|
+
const key = [
|
|
34
|
+
normalizeString(participant.conversationId),
|
|
35
|
+
normalizeString(participant.participantId),
|
|
36
|
+
normalizeString(participant.label),
|
|
37
|
+
normalizeString(participant.role),
|
|
38
|
+
].join('::');
|
|
39
|
+
if (seen.has(key))
|
|
40
|
+
return;
|
|
41
|
+
seen.add(key);
|
|
42
|
+
result.push(participant);
|
|
43
|
+
};
|
|
44
|
+
for (const participant of primary ?? [])
|
|
45
|
+
pushParticipant(participant);
|
|
46
|
+
for (const participant of secondary ?? [])
|
|
47
|
+
pushParticipant(participant);
|
|
48
|
+
return result.length > 0 ? result : undefined;
|
|
49
|
+
}
|
|
50
|
+
function mergeTwoWorkSessions(primary, secondary) {
|
|
51
|
+
const disclosureNotes = mergeStringLists(primary.disclosureNotes, secondary.disclosureNotes);
|
|
52
|
+
const visibleFacts = mergeStringLists(primary.visibleFacts, secondary.visibleFacts);
|
|
53
|
+
const pendingQuestions = mergeStringLists(primary.pendingQuestions, secondary.pendingQuestions);
|
|
54
|
+
const participants = mergeParticipants(primary.participants, secondary.participants);
|
|
55
|
+
return {
|
|
56
|
+
id: primary.id,
|
|
57
|
+
title: primary.title ?? secondary.title ?? null,
|
|
58
|
+
objective: primary.objective ?? secondary.objective ?? null,
|
|
59
|
+
status: primary.status ?? secondary.status ?? null,
|
|
60
|
+
summary: primary.summary ?? secondary.summary ?? null,
|
|
61
|
+
representation: primary.representation ?? secondary.representation ?? null,
|
|
62
|
+
currentConversationRole: primary.currentConversationRole ?? secondary.currentConversationRole ?? null,
|
|
63
|
+
sourceConversationId: primary.sourceConversationId ?? secondary.sourceConversationId ?? null,
|
|
64
|
+
sourceConversationLabel: primary.sourceConversationLabel ?? secondary.sourceConversationLabel ?? null,
|
|
65
|
+
sourceParticipantId: primary.sourceParticipantId ?? secondary.sourceParticipantId ?? null,
|
|
66
|
+
sourceParticipantLabel: primary.sourceParticipantLabel ?? secondary.sourceParticipantLabel ?? null,
|
|
67
|
+
disclosure: primary.disclosure ?? secondary.disclosure ?? null,
|
|
68
|
+
...(disclosureNotes ? { disclosureNotes } : {}),
|
|
69
|
+
...(visibleFacts ? { visibleFacts } : {}),
|
|
70
|
+
...(pendingQuestions ? { pendingQuestions } : {}),
|
|
71
|
+
...(participants ? { participants } : {}),
|
|
72
|
+
updatedAt: primary.updatedAt ?? secondary.updatedAt ?? null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function mergeWorkSessionContexts(explicitWorkSession, activeWorkSessions) {
|
|
76
|
+
const merged = new Map();
|
|
77
|
+
const add = (workSession) => {
|
|
78
|
+
if (!workSession?.id)
|
|
79
|
+
return;
|
|
80
|
+
const existing = merged.get(workSession.id);
|
|
81
|
+
merged.set(workSession.id, existing ? mergeTwoWorkSessions(existing, workSession) : workSession);
|
|
82
|
+
};
|
|
83
|
+
add(explicitWorkSession);
|
|
84
|
+
for (const workSession of activeWorkSessions ?? []) {
|
|
85
|
+
add(workSession);
|
|
86
|
+
}
|
|
87
|
+
return Array.from(merged.values());
|
|
88
|
+
}
|
|
89
|
+
function formatParticipantLabel(participant) {
|
|
90
|
+
const label = normalizeString(participant.label);
|
|
91
|
+
if (label)
|
|
92
|
+
return label;
|
|
93
|
+
const participantId = normalizeString(participant.participantId);
|
|
94
|
+
if (participantId)
|
|
95
|
+
return participantId;
|
|
96
|
+
return normalizeString(participant.conversationId);
|
|
97
|
+
}
|
|
98
|
+
function resolvePromptOptions(options) {
|
|
99
|
+
return {
|
|
100
|
+
maxVisibleFacts: Math.max(0, options?.maxVisibleFacts
|
|
101
|
+
?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxVisibleFacts),
|
|
102
|
+
maxPendingQuestions: Math.max(0, options?.maxPendingQuestions
|
|
103
|
+
?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxPendingQuestions),
|
|
104
|
+
maxDisclosureNotes: Math.max(0, options?.maxDisclosureNotes
|
|
105
|
+
?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxDisclosureNotes),
|
|
106
|
+
maxParticipants: Math.max(0, options?.maxParticipants ?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxParticipants),
|
|
107
|
+
maxDetailedWorkSessions: Math.max(1, options?.maxDetailedWorkSessions
|
|
108
|
+
?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxDetailedWorkSessions),
|
|
109
|
+
maxOtherWorkSessions: Math.max(0, options?.maxOtherWorkSessions
|
|
110
|
+
?? DEFAULT_WORK_SESSION_PROMPT_OPTIONS.maxOtherWorkSessions),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function takeWithHiddenCount(values, maxCount) {
|
|
114
|
+
if (maxCount <= 0) {
|
|
115
|
+
return { visible: [], hiddenCount: values.length };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
visible: values.slice(0, maxCount),
|
|
119
|
+
hiddenCount: Math.max(0, values.length - maxCount),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function buildOriginLabel(workSession) {
|
|
123
|
+
const sourceConversation = normalizeString(workSession.sourceConversationLabel)
|
|
124
|
+
?? normalizeString(workSession.sourceConversationId);
|
|
125
|
+
const sourceParticipant = normalizeString(workSession.sourceParticipantLabel)
|
|
126
|
+
?? normalizeString(workSession.sourceParticipantId);
|
|
127
|
+
if (sourceConversation && sourceParticipant) {
|
|
128
|
+
return `${sourceParticipant} in ${sourceConversation}`;
|
|
129
|
+
}
|
|
130
|
+
return sourceParticipant ?? sourceConversation;
|
|
131
|
+
}
|
|
132
|
+
function formatWorkSessionReference(workSession) {
|
|
133
|
+
const title = normalizeString(workSession.title);
|
|
134
|
+
const objective = normalizeString(workSession.objective);
|
|
135
|
+
if (title)
|
|
136
|
+
return `${title} (${workSession.id})`;
|
|
137
|
+
if (objective)
|
|
138
|
+
return `${objective} (${workSession.id})`;
|
|
139
|
+
return workSession.id;
|
|
140
|
+
}
|
|
141
|
+
export function buildWorkSessionPromptLines(workSession, options) {
|
|
142
|
+
if (!workSession)
|
|
143
|
+
return [];
|
|
144
|
+
const promptOptions = resolvePromptOptions(options);
|
|
145
|
+
const title = normalizeString(workSession.title);
|
|
146
|
+
const objective = normalizeString(workSession.objective);
|
|
147
|
+
const summary = normalizeString(workSession.summary);
|
|
148
|
+
const origin = buildOriginLabel(workSession);
|
|
149
|
+
const visibleFacts = takeWithHiddenCount(normalizeStringList(workSession.visibleFacts), promptOptions.maxVisibleFacts);
|
|
150
|
+
const pendingQuestions = takeWithHiddenCount(normalizeStringList(workSession.pendingQuestions), promptOptions.maxPendingQuestions);
|
|
151
|
+
const disclosureNotes = takeWithHiddenCount(normalizeStringList(workSession.disclosureNotes), promptOptions.maxDisclosureNotes);
|
|
152
|
+
const participants = takeWithHiddenCount(Array.isArray(workSession.participants) ? workSession.participants : [], promptOptions.maxParticipants);
|
|
153
|
+
return [
|
|
154
|
+
`Canon work session ID: ${workSession.id}`,
|
|
155
|
+
...(title ? [`Canon work session title: ${title}`] : []),
|
|
156
|
+
...(objective ? [`Canon work session objective: ${objective}`] : []),
|
|
157
|
+
...(workSession.status
|
|
158
|
+
? [`Canon work session status: ${workSession.status}`]
|
|
159
|
+
: []),
|
|
160
|
+
...(workSession.currentConversationRole
|
|
161
|
+
? [
|
|
162
|
+
`This conversation's role in the work session: ${workSession.currentConversationRole}`,
|
|
163
|
+
]
|
|
164
|
+
: []),
|
|
165
|
+
...(workSession.representation
|
|
166
|
+
? [`Participate in this conversation as: ${workSession.representation}`]
|
|
167
|
+
: []),
|
|
168
|
+
...(origin ? [`Work session origin: ${origin}`] : []),
|
|
169
|
+
...(workSession.disclosure
|
|
170
|
+
? [`Allowed disclosure level in this conversation: ${workSession.disclosure}`]
|
|
171
|
+
: []),
|
|
172
|
+
...(summary ? [`Shared work session summary: ${summary}`] : []),
|
|
173
|
+
...visibleFacts.visible.map((fact, index) => `Shared fact ${index + 1}: ${fact}`),
|
|
174
|
+
...(visibleFacts.hiddenCount > 0
|
|
175
|
+
? [`Additional shared facts omitted for brevity: ${visibleFacts.hiddenCount}`]
|
|
176
|
+
: []),
|
|
177
|
+
...pendingQuestions.visible.map((question, index) => `Open question ${index + 1}: ${question}`),
|
|
178
|
+
...(pendingQuestions.hiddenCount > 0
|
|
179
|
+
? [
|
|
180
|
+
`Additional open questions omitted for brevity: ${pendingQuestions.hiddenCount}`,
|
|
181
|
+
]
|
|
182
|
+
: []),
|
|
183
|
+
...disclosureNotes.visible.map((note, index) => `Disclosure note ${index + 1}: ${note}`),
|
|
184
|
+
...(disclosureNotes.hiddenCount > 0
|
|
185
|
+
? [
|
|
186
|
+
`Additional disclosure notes omitted for brevity: ${disclosureNotes.hiddenCount}`,
|
|
187
|
+
]
|
|
188
|
+
: []),
|
|
189
|
+
...participants.visible.flatMap((participant, index) => {
|
|
190
|
+
const label = formatParticipantLabel(participant);
|
|
191
|
+
if (!label)
|
|
192
|
+
return [];
|
|
193
|
+
const role = normalizeString(participant.role);
|
|
194
|
+
return [
|
|
195
|
+
`Related participant ${index + 1}: ${label}${role ? ` (${role})` : ''}`,
|
|
196
|
+
];
|
|
197
|
+
}),
|
|
198
|
+
...(participants.hiddenCount > 0
|
|
199
|
+
? [
|
|
200
|
+
`Additional related participants omitted for brevity: ${participants.hiddenCount}`,
|
|
201
|
+
]
|
|
202
|
+
: []),
|
|
203
|
+
'Canon rule: this shared work-session context is scoped to this conversation only and does not grant access to other conversation transcripts.',
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
export function buildWorkSessionsPromptLines(workSessions, options) {
|
|
207
|
+
const promptOptions = resolvePromptOptions(options);
|
|
208
|
+
if (!Array.isArray(workSessions) || workSessions.length === 0)
|
|
209
|
+
return [];
|
|
210
|
+
if (workSessions.length === 1) {
|
|
211
|
+
return [
|
|
212
|
+
'Canon active work session for this conversation:',
|
|
213
|
+
...buildWorkSessionPromptLines(workSessions[0], promptOptions),
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
const detailedWorkSessions = workSessions.slice(0, promptOptions.maxDetailedWorkSessions);
|
|
217
|
+
const hiddenWorkSessions = workSessions.slice(promptOptions.maxDetailedWorkSessions);
|
|
218
|
+
const lines = ['Canon active work sessions for this conversation:'];
|
|
219
|
+
detailedWorkSessions.forEach((workSession, index) => {
|
|
220
|
+
lines.push(detailedWorkSessions.length === 1
|
|
221
|
+
? 'Primary active work session:'
|
|
222
|
+
: `Detailed work session ${index + 1}:`);
|
|
223
|
+
lines.push(...buildWorkSessionPromptLines(workSession, promptOptions));
|
|
224
|
+
});
|
|
225
|
+
if (hiddenWorkSessions.length > 0) {
|
|
226
|
+
lines.push('Other active work sessions linked here:');
|
|
227
|
+
hiddenWorkSessions
|
|
228
|
+
.slice(0, promptOptions.maxOtherWorkSessions)
|
|
229
|
+
.forEach((workSession, index) => {
|
|
230
|
+
lines.push(`Other work session ${index + 1}: ${formatWorkSessionReference(workSession)}`);
|
|
231
|
+
});
|
|
232
|
+
if (hiddenWorkSessions.length > promptOptions.maxOtherWorkSessions) {
|
|
233
|
+
lines.push(`Additional active work sessions omitted for brevity: ${hiddenWorkSessions.length - promptOptions.maxOtherWorkSessions}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push('Canon rule: do not assume the current message refers to another linked work session unless the conversation clearly points to it.');
|
|
236
|
+
}
|
|
237
|
+
return lines;
|
|
238
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js",
|
|
11
12
|
"types": "./dist/index.d.ts"
|
|
12
13
|
},
|
|
13
14
|
"./browser": {
|
|
14
15
|
"import": "./dist/browser.js",
|
|
16
|
+
"default": "./dist/browser.js",
|
|
15
17
|
"types": "./dist/browser.d.ts"
|
|
16
18
|
}
|
|
17
19
|
},
|