@canonmsg/core 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-resolver.d.ts +4 -0
- package/dist/agent-resolver.js +38 -15
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +3 -0
- package/dist/client.d.ts +6 -2
- package/dist/client.js +21 -2
- package/dist/execution-environment.d.ts +59 -0
- package/dist/execution-environment.js +284 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -2
- package/dist/policy.d.ts +156 -0
- package/dist/policy.js +189 -0
- package/dist/rtdb-rest.d.ts +24 -0
- package/dist/rtdb-rest.js +40 -0
- package/dist/turn-protocol.d.ts +56 -0
- package/dist/turn-protocol.js +152 -0
- package/dist/types.d.ts +31 -8
- package/dist/types.js +7 -6
- package/package.json +7 -3
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export type PolicyRole = "owner" | "conversation_member" | "group_admin" | "external_requester" | "agent_self";
|
|
2
|
+
export type ParticipationStyle = "natural" | "collaborative" | "mention-first" | "approval-gated" | "handoff-only" | "observer";
|
|
3
|
+
export type RepresentationMode = "self" | "delegate";
|
|
4
|
+
export type PermissionLevel = "deny" | "allow" | "require_approval";
|
|
5
|
+
export type ConversationScope = "global" | "relationship" | "conversation" | "work_session" | "message";
|
|
6
|
+
export interface Participant {
|
|
7
|
+
id: string;
|
|
8
|
+
type: "human" | "ai_agent";
|
|
9
|
+
ownerId?: string | null;
|
|
10
|
+
relationshipIds?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface Relationship {
|
|
13
|
+
id: string;
|
|
14
|
+
participantIds: [string, string];
|
|
15
|
+
profileId?: string | null;
|
|
16
|
+
notes?: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface ContextOverlay {
|
|
19
|
+
scope: ConversationScope;
|
|
20
|
+
scopeId: string;
|
|
21
|
+
instructions?: string | null;
|
|
22
|
+
notes?: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface AgentBehaviorSettings {
|
|
25
|
+
participationStyle?: ParticipationStyle | null;
|
|
26
|
+
allowAgentToAgent?: boolean | null;
|
|
27
|
+
allowLongRunningCollaboration?: boolean | null;
|
|
28
|
+
requireMentionForGroupReplies?: boolean | null;
|
|
29
|
+
/** @deprecated Use requireMentionForGroupReplies instead. */
|
|
30
|
+
requireMentionForGroupAgentReplies?: boolean | null;
|
|
31
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
32
|
+
instructions?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface BehaviorProfile {
|
|
35
|
+
id: string;
|
|
36
|
+
label: string;
|
|
37
|
+
participationStyle: ParticipationStyle;
|
|
38
|
+
defaultRepresentation: RepresentationMode;
|
|
39
|
+
allowAgentToAgent: boolean;
|
|
40
|
+
allowLongRunningCollaboration: boolean;
|
|
41
|
+
requireMentionForGroupReplies: boolean;
|
|
42
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
43
|
+
}
|
|
44
|
+
export interface AdmissionPolicy {
|
|
45
|
+
canDiscover: PermissionLevel;
|
|
46
|
+
canStartDirectConversation: PermissionLevel;
|
|
47
|
+
canAddToGroup: PermissionLevel;
|
|
48
|
+
canSendContactRequest: PermissionLevel;
|
|
49
|
+
}
|
|
50
|
+
export interface RuntimeControlPolicy {
|
|
51
|
+
canInterrupt: PermissionLevel;
|
|
52
|
+
canChangeModel: PermissionLevel;
|
|
53
|
+
canChangeWorkspace: PermissionLevel;
|
|
54
|
+
canChangeSessionConfig: PermissionLevel;
|
|
55
|
+
}
|
|
56
|
+
export interface ActionApprovalPolicy {
|
|
57
|
+
canInitiateExternalFirstContact: PermissionLevel;
|
|
58
|
+
canShareIdentity: PermissionLevel;
|
|
59
|
+
canIntroduceParticipant: PermissionLevel;
|
|
60
|
+
canSpeakAsDelegate: PermissionLevel;
|
|
61
|
+
}
|
|
62
|
+
export interface ParticipationPolicy {
|
|
63
|
+
style: ParticipationStyle;
|
|
64
|
+
allowAgentToAgent: boolean;
|
|
65
|
+
allowHumanToAgent: boolean;
|
|
66
|
+
allowLongRunningCollaboration: boolean;
|
|
67
|
+
requireMentionForGroupReplies: boolean;
|
|
68
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
69
|
+
}
|
|
70
|
+
export interface WorkSession {
|
|
71
|
+
id: string;
|
|
72
|
+
conversationId?: string | null;
|
|
73
|
+
label?: string | null;
|
|
74
|
+
objective?: string | null;
|
|
75
|
+
participationStyle?: ParticipationStyle;
|
|
76
|
+
activeParticipantIds: string[];
|
|
77
|
+
overlayIds?: string[];
|
|
78
|
+
status: "active" | "paused" | "completed";
|
|
79
|
+
}
|
|
80
|
+
export interface ResolvedPolicy {
|
|
81
|
+
admission: AdmissionPolicy;
|
|
82
|
+
runtime: RuntimeControlPolicy;
|
|
83
|
+
actionApproval: ActionApprovalPolicy;
|
|
84
|
+
participation: ParticipationPolicy;
|
|
85
|
+
representation: {
|
|
86
|
+
defaultMode: RepresentationMode;
|
|
87
|
+
};
|
|
88
|
+
overlays: ContextOverlay[];
|
|
89
|
+
resolvedFrom: {
|
|
90
|
+
globalDefault?: string;
|
|
91
|
+
agentDefault?: string;
|
|
92
|
+
relationship?: string;
|
|
93
|
+
workSession?: string;
|
|
94
|
+
messageDirective?: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export interface ResolvedTurnEligibility {
|
|
98
|
+
mayAutoReply: boolean;
|
|
99
|
+
mayInterrupt: boolean;
|
|
100
|
+
mayQueueWhileRunning: boolean;
|
|
101
|
+
mayInterruptRunningTurn: boolean;
|
|
102
|
+
mayInterleaveWhileRunning: boolean;
|
|
103
|
+
mayReactToNonFinalAgentMessages: boolean;
|
|
104
|
+
mayChangeModel: boolean;
|
|
105
|
+
mayChangeWorkspace: boolean;
|
|
106
|
+
maySpeakAsDelegate: boolean;
|
|
107
|
+
mayInitiateExternalFirstContact: boolean;
|
|
108
|
+
reasonCodes: string[];
|
|
109
|
+
}
|
|
110
|
+
export interface ResolvedAgentBehaviorPolicy {
|
|
111
|
+
participation: ParticipationPolicy;
|
|
112
|
+
instructions: string[];
|
|
113
|
+
source: {
|
|
114
|
+
hasAgentDefault: boolean;
|
|
115
|
+
hasConversationOverride: boolean;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export interface ParticipationDecisionInput {
|
|
119
|
+
conversationType: "direct" | "group" | "unknown";
|
|
120
|
+
senderType: "human" | "ai_agent";
|
|
121
|
+
isOwner: boolean;
|
|
122
|
+
mentionedAgent: boolean;
|
|
123
|
+
recentHumanCount?: number;
|
|
124
|
+
consecutiveAgentTurns?: number;
|
|
125
|
+
currentAgentStreakStartedByHuman?: boolean;
|
|
126
|
+
}
|
|
127
|
+
export interface ParticipationDecision {
|
|
128
|
+
allow: boolean;
|
|
129
|
+
reasonCode: string;
|
|
130
|
+
reason: string;
|
|
131
|
+
}
|
|
132
|
+
export interface ParticipationHistoryMessage {
|
|
133
|
+
id?: string;
|
|
134
|
+
senderId: string;
|
|
135
|
+
senderType: "human" | "ai_agent";
|
|
136
|
+
metadata?: unknown;
|
|
137
|
+
}
|
|
138
|
+
export interface ParticipationHistorySnapshot {
|
|
139
|
+
recentSenderTypes: Array<"human" | "ai_agent">;
|
|
140
|
+
recentHumanCount: number;
|
|
141
|
+
recentAgentCount: number;
|
|
142
|
+
consecutiveAgentTurns: number;
|
|
143
|
+
currentAgentStreakStartedByHuman: boolean;
|
|
144
|
+
}
|
|
145
|
+
export declare const DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
146
|
+
export declare function getDefaultParticipationPolicy(): ParticipationPolicy;
|
|
147
|
+
export declare function resolveAgentBehaviorPolicy(input?: {
|
|
148
|
+
agentDefault?: AgentBehaviorSettings | null;
|
|
149
|
+
conversationOverride?: AgentBehaviorSettings | null;
|
|
150
|
+
}): ResolvedAgentBehaviorPolicy;
|
|
151
|
+
export declare function evaluateParticipationPolicy(policy: ResolvedAgentBehaviorPolicy | null | undefined, input: ParticipationDecisionInput): ParticipationDecision;
|
|
152
|
+
export declare function buildBehaviorPolicyLines(policy: ResolvedAgentBehaviorPolicy | null | undefined): string[];
|
|
153
|
+
export declare function buildParticipationHistorySnapshot(messages: ParticipationHistoryMessage[], agentId: string): ParticipationHistorySnapshot;
|
|
154
|
+
export declare function buildParticipationHistorySnapshots<TMessage extends ParticipationHistoryMessage & {
|
|
155
|
+
id: string;
|
|
156
|
+
}>(messages: TMessage[], agentId: string): Map<string, ParticipationHistorySnapshot>;
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { resolveTurnMessageSemantics } from "./turn-protocol.js";
|
|
2
|
+
export const DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
3
|
+
const DEFAULT_PARTICIPATION_POLICY = {
|
|
4
|
+
style: "natural",
|
|
5
|
+
allowAgentToAgent: true,
|
|
6
|
+
allowHumanToAgent: true,
|
|
7
|
+
allowLongRunningCollaboration: true,
|
|
8
|
+
requireMentionForGroupReplies: false,
|
|
9
|
+
maxConsecutiveAgentTurns: null,
|
|
10
|
+
};
|
|
11
|
+
function coalesceBoolean(overrideValue, fallbackValue, defaultValue = false) {
|
|
12
|
+
if (typeof overrideValue === "boolean")
|
|
13
|
+
return overrideValue;
|
|
14
|
+
if (typeof fallbackValue === "boolean")
|
|
15
|
+
return fallbackValue;
|
|
16
|
+
return defaultValue;
|
|
17
|
+
}
|
|
18
|
+
function coalesceStyle(overrideValue, fallbackValue) {
|
|
19
|
+
return overrideValue ?? fallbackValue ?? DEFAULT_PARTICIPATION_POLICY.style;
|
|
20
|
+
}
|
|
21
|
+
function resolveGroupMentionRequirement(value) {
|
|
22
|
+
if (typeof value?.requireMentionForGroupReplies === "boolean") {
|
|
23
|
+
return value.requireMentionForGroupReplies;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value?.requireMentionForGroupAgentReplies === "boolean") {
|
|
26
|
+
return value.requireMentionForGroupAgentReplies;
|
|
27
|
+
}
|
|
28
|
+
if (value?.requireMentionForGroupReplies === null ||
|
|
29
|
+
value?.requireMentionForGroupAgentReplies === null) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function coalesceMaxConsecutiveAgentTurns(overrideValue, fallbackValue) {
|
|
35
|
+
if (overrideValue === null)
|
|
36
|
+
return null;
|
|
37
|
+
if (typeof overrideValue === "number")
|
|
38
|
+
return overrideValue;
|
|
39
|
+
if (fallbackValue === null)
|
|
40
|
+
return null;
|
|
41
|
+
if (typeof fallbackValue === "number")
|
|
42
|
+
return fallbackValue;
|
|
43
|
+
return DEFAULT_PARTICIPATION_POLICY.maxConsecutiveAgentTurns ?? null;
|
|
44
|
+
}
|
|
45
|
+
function normalizedInstructions(value) {
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
50
|
+
}
|
|
51
|
+
export function getDefaultParticipationPolicy() {
|
|
52
|
+
return { ...DEFAULT_PARTICIPATION_POLICY };
|
|
53
|
+
}
|
|
54
|
+
export function resolveAgentBehaviorPolicy(input) {
|
|
55
|
+
const agentDefault = input?.agentDefault ?? null;
|
|
56
|
+
const conversationOverride = input?.conversationOverride ?? null;
|
|
57
|
+
const defaultInstructions = normalizedInstructions(agentDefault?.instructions);
|
|
58
|
+
const overrideInstructions = normalizedInstructions(conversationOverride?.instructions);
|
|
59
|
+
return {
|
|
60
|
+
participation: {
|
|
61
|
+
style: coalesceStyle(conversationOverride?.participationStyle, agentDefault?.participationStyle),
|
|
62
|
+
allowAgentToAgent: coalesceBoolean(conversationOverride?.allowAgentToAgent, agentDefault?.allowAgentToAgent, DEFAULT_PARTICIPATION_POLICY.allowAgentToAgent),
|
|
63
|
+
allowHumanToAgent: DEFAULT_PARTICIPATION_POLICY.allowHumanToAgent,
|
|
64
|
+
allowLongRunningCollaboration: coalesceBoolean(conversationOverride?.allowLongRunningCollaboration, agentDefault?.allowLongRunningCollaboration, DEFAULT_PARTICIPATION_POLICY.allowLongRunningCollaboration),
|
|
65
|
+
requireMentionForGroupReplies: coalesceBoolean(resolveGroupMentionRequirement(conversationOverride), resolveGroupMentionRequirement(agentDefault), DEFAULT_PARTICIPATION_POLICY.requireMentionForGroupReplies),
|
|
66
|
+
maxConsecutiveAgentTurns: coalesceMaxConsecutiveAgentTurns(conversationOverride?.maxConsecutiveAgentTurns, agentDefault?.maxConsecutiveAgentTurns),
|
|
67
|
+
},
|
|
68
|
+
instructions: [
|
|
69
|
+
...(defaultInstructions ? [defaultInstructions] : []),
|
|
70
|
+
...(overrideInstructions ? [overrideInstructions] : []),
|
|
71
|
+
],
|
|
72
|
+
source: {
|
|
73
|
+
hasAgentDefault: agentDefault != null,
|
|
74
|
+
hasConversationOverride: conversationOverride != null,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function evaluateParticipationPolicy(policy, input) {
|
|
79
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
80
|
+
const participation = resolved.participation;
|
|
81
|
+
const consecutiveAgentTurns = Math.max(input.consecutiveAgentTurns ?? (input.senderType === "ai_agent" ? 1 : 0), input.senderType === "ai_agent" ? 1 : 0);
|
|
82
|
+
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
83
|
+
if (input.isOwner) {
|
|
84
|
+
return {
|
|
85
|
+
allow: true,
|
|
86
|
+
reasonCode: "owner_sender",
|
|
87
|
+
reason: "owner messages always pass through",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (input.conversationType === "group" &&
|
|
91
|
+
participation.requireMentionForGroupReplies &&
|
|
92
|
+
!input.mentionedAgent) {
|
|
93
|
+
return {
|
|
94
|
+
allow: false,
|
|
95
|
+
reasonCode: "group_mention_required",
|
|
96
|
+
reason: "group replies require a direct mention",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (input.senderType !== "ai_agent") {
|
|
100
|
+
return {
|
|
101
|
+
allow: true,
|
|
102
|
+
reasonCode: "human_sender",
|
|
103
|
+
reason: "latest sender is human",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (!participation.allowAgentToAgent) {
|
|
107
|
+
return {
|
|
108
|
+
allow: false,
|
|
109
|
+
reasonCode: "agent_to_agent_disabled",
|
|
110
|
+
reason: "agent-to-agent participation is disabled by policy",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!participation.allowLongRunningCollaboration &&
|
|
114
|
+
(consecutiveAgentTurns > 1 || !currentAgentStreakStartedByHuman)) {
|
|
115
|
+
return {
|
|
116
|
+
allow: false,
|
|
117
|
+
reasonCode: "human_reset_required",
|
|
118
|
+
reason: "a fresh human steer is required before continuing agent collaboration",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (typeof participation.maxConsecutiveAgentTurns === "number" &&
|
|
122
|
+
participation.maxConsecutiveAgentTurns >= 0 &&
|
|
123
|
+
consecutiveAgentTurns > participation.maxConsecutiveAgentTurns) {
|
|
124
|
+
return {
|
|
125
|
+
allow: false,
|
|
126
|
+
reasonCode: "agent_turn_limit_reached",
|
|
127
|
+
reason: "maximum consecutive agent turns reached",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
allow: true,
|
|
132
|
+
reasonCode: input.conversationType === "group"
|
|
133
|
+
? "group_agent_allowed"
|
|
134
|
+
: "direct_agent_allowed",
|
|
135
|
+
reason: participation.requireMentionForGroupReplies
|
|
136
|
+
? "policy allows this directly mentioned group reply"
|
|
137
|
+
: "agent-to-agent participation allowed by policy",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function buildBehaviorPolicyLines(policy) {
|
|
141
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
142
|
+
const maxTurns = resolved.participation.maxConsecutiveAgentTurns;
|
|
143
|
+
return [
|
|
144
|
+
`Canon guidance: participation posture is ${resolved.participation.style}.`,
|
|
145
|
+
`Canon rule: group replies require direct mention: ${resolved.participation.requireMentionForGroupReplies ? "yes" : "no"}`,
|
|
146
|
+
`Canon rule: agent-to-agent collaboration allowed: ${resolved.participation.allowAgentToAgent ? "yes" : "no"}`,
|
|
147
|
+
`Canon rule: long-running collaboration allowed: ${resolved.participation.allowLongRunningCollaboration ? "yes" : "no"}`,
|
|
148
|
+
`Canon rule: max consecutive agent turns without a human reset: ${typeof maxTurns === "number" ? String(maxTurns) : "unlimited"}`,
|
|
149
|
+
...resolved.instructions.map((instruction, index) => `Canon guidance ${index + 1}: ${instruction}`),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
function toHistorySenderType(message) {
|
|
153
|
+
if (message.senderType !== "ai_agent") {
|
|
154
|
+
return "human";
|
|
155
|
+
}
|
|
156
|
+
return resolveTurnMessageSemantics({
|
|
157
|
+
senderType: message.senderType,
|
|
158
|
+
metadata: message.metadata,
|
|
159
|
+
}) === "turn_complete"
|
|
160
|
+
? "ai_agent"
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
export function buildParticipationHistorySnapshot(messages, agentId) {
|
|
164
|
+
const recentSenderTypes = messages.flatMap((message) => {
|
|
165
|
+
const senderType = toHistorySenderType(message);
|
|
166
|
+
return senderType ? [senderType] : [];
|
|
167
|
+
});
|
|
168
|
+
let consecutiveAgentTurns = 0;
|
|
169
|
+
for (const senderType of recentSenderTypes) {
|
|
170
|
+
if (senderType !== "ai_agent")
|
|
171
|
+
break;
|
|
172
|
+
consecutiveAgentTurns += 1;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
recentSenderTypes,
|
|
176
|
+
recentHumanCount: recentSenderTypes.filter((senderType) => senderType === "human").length,
|
|
177
|
+
recentAgentCount: recentSenderTypes.filter((senderType) => senderType === "ai_agent").length,
|
|
178
|
+
consecutiveAgentTurns,
|
|
179
|
+
currentAgentStreakStartedByHuman: consecutiveAgentTurns > 0 &&
|
|
180
|
+
recentSenderTypes[consecutiveAgentTurns] === "human",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function buildParticipationHistorySnapshots(messages, agentId) {
|
|
184
|
+
const snapshots = new Map();
|
|
185
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
186
|
+
snapshots.set(messages[index].id, buildParticipationHistorySnapshot(messages.slice(index + 1), agentId));
|
|
187
|
+
}
|
|
188
|
+
return snapshots;
|
|
189
|
+
}
|
package/dist/rtdb-rest.d.ts
CHANGED
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
* is exchanged for a Firebase ID token before use with RTDB REST.
|
|
7
7
|
*/
|
|
8
8
|
import type { CanonClient } from './client.js';
|
|
9
|
+
import type { DeliveryIntent, RuntimeCapabilities, TurnLifecycleState } from './turn-protocol.js';
|
|
9
10
|
export interface SessionStatePayload {
|
|
11
|
+
lastError?: string;
|
|
10
12
|
model?: string;
|
|
11
13
|
permissionMode?: string;
|
|
12
14
|
effort?: string;
|
|
13
15
|
cwd?: string;
|
|
16
|
+
executionMode?: 'worktree' | 'locked';
|
|
17
|
+
executionBranch?: string;
|
|
14
18
|
hostMode?: boolean;
|
|
15
19
|
clientType?: string;
|
|
16
20
|
isActive: boolean;
|
|
@@ -28,6 +32,24 @@ export interface SessionStatePayload {
|
|
|
28
32
|
'.sv': 'timestamp';
|
|
29
33
|
};
|
|
30
34
|
}
|
|
35
|
+
export interface TurnStatePayload {
|
|
36
|
+
turnId?: string | null;
|
|
37
|
+
state: TurnLifecycleState;
|
|
38
|
+
queueDepth: number;
|
|
39
|
+
currentSpeakerId?: string | null;
|
|
40
|
+
lastAcceptedIntent?: DeliveryIntent | null;
|
|
41
|
+
activeMessageIds?: string[];
|
|
42
|
+
capabilities?: RuntimeCapabilities;
|
|
43
|
+
openedAt?: number | {
|
|
44
|
+
'.sv': 'timestamp';
|
|
45
|
+
};
|
|
46
|
+
completedAt?: number | {
|
|
47
|
+
'.sv': 'timestamp';
|
|
48
|
+
} | null;
|
|
49
|
+
updatedAt: {
|
|
50
|
+
'.sv': 'timestamp';
|
|
51
|
+
};
|
|
52
|
+
}
|
|
31
53
|
/** Must be called once before any RTDB operations. */
|
|
32
54
|
export declare function initRTDBAuth(client: CanonClient): void;
|
|
33
55
|
/** Generic RTDB REST write (PUT). */
|
|
@@ -43,3 +65,5 @@ export declare function writeSessionState(conversationId: string, agentId: strin
|
|
|
43
65
|
* Clear session state from RTDB (full overwrite with isActive: false).
|
|
44
66
|
*/
|
|
45
67
|
export declare function clearSessionState(conversationId: string, agentId: string): Promise<void>;
|
|
68
|
+
export declare function writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
|
|
69
|
+
export declare function clearTurnState(conversationId: string, agentId: string): Promise<void>;
|
package/dist/rtdb-rest.js
CHANGED
|
@@ -126,3 +126,43 @@ export async function clearSessionState(conversationId, agentId) {
|
|
|
126
126
|
console.error(`[canon] RTDB clear failed (${res.status}): ${text}`);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
export async function writeTurnState(conversationId, agentId, state) {
|
|
130
|
+
const token = await getToken();
|
|
131
|
+
if (!token)
|
|
132
|
+
return;
|
|
133
|
+
const url = `${RTDB_BASE}/turn-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
134
|
+
const body = {
|
|
135
|
+
...state,
|
|
136
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
137
|
+
};
|
|
138
|
+
const res = await fetch(url, {
|
|
139
|
+
method: 'PUT',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
body: JSON.stringify(body),
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
const text = await res.text();
|
|
145
|
+
throw new Error(`RTDB write failed (${res.status}): ${text}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function clearTurnState(conversationId, agentId) {
|
|
149
|
+
const token = await getToken();
|
|
150
|
+
if (!token)
|
|
151
|
+
return;
|
|
152
|
+
const url = `${RTDB_BASE}/turn-state/${conversationId}/${agentId}.json?auth=${token}`;
|
|
153
|
+
const body = {
|
|
154
|
+
state: 'idle',
|
|
155
|
+
queueDepth: 0,
|
|
156
|
+
updatedAt: { '.sv': 'timestamp' },
|
|
157
|
+
completedAt: { '.sv': 'timestamp' },
|
|
158
|
+
};
|
|
159
|
+
const res = await fetch(url, {
|
|
160
|
+
method: 'PUT',
|
|
161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify(body),
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const text = await res.text();
|
|
166
|
+
console.error(`[canon] RTDB turn clear failed (${res.status}): ${text}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type DeliveryIntent = 'queue' | 'interrupt' | 'interleave' | 'stop';
|
|
2
|
+
export type TurnMessageSemantics = 'progress' | 'turn_complete' | 'control';
|
|
3
|
+
export type InboundDisposition = 'queued' | 'accepted_now' | 'interleaved' | 'trigger_suppressed' | 'rejected';
|
|
4
|
+
export type TurnLifecycleState = 'idle' | 'thinking' | 'streaming' | 'tool' | 'waiting_input' | 'completed' | 'interrupted';
|
|
5
|
+
export interface RuntimeCapabilities {
|
|
6
|
+
supportsInterrupt: boolean;
|
|
7
|
+
supportsQueue: boolean;
|
|
8
|
+
supportsInterleave: boolean;
|
|
9
|
+
supportsRequiresAction: boolean;
|
|
10
|
+
supportsNonFinalPermanentMessages: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const FINAL_MESSAGE_HANDOFF_MS = 750;
|
|
13
|
+
export interface TurnState {
|
|
14
|
+
turnId?: string | null;
|
|
15
|
+
state: TurnLifecycleState;
|
|
16
|
+
queueDepth: number;
|
|
17
|
+
currentSpeakerId?: string | null;
|
|
18
|
+
lastAcceptedIntent?: DeliveryIntent | null;
|
|
19
|
+
activeMessageIds?: string[];
|
|
20
|
+
capabilities?: RuntimeCapabilities;
|
|
21
|
+
openedAt?: number;
|
|
22
|
+
updatedAt?: number;
|
|
23
|
+
completedAt?: number | null;
|
|
24
|
+
}
|
|
25
|
+
export interface TurnMetadata {
|
|
26
|
+
turnId?: string | null;
|
|
27
|
+
turnSemantics?: TurnMessageSemantics;
|
|
28
|
+
deliveryIntent?: DeliveryIntent;
|
|
29
|
+
turnComplete?: boolean;
|
|
30
|
+
replyBehavior?: 'allow_auto_reply' | 'suppress_auto_reply';
|
|
31
|
+
inboundDisposition?: InboundDisposition;
|
|
32
|
+
}
|
|
33
|
+
export interface TriggerDecision {
|
|
34
|
+
allow: boolean;
|
|
35
|
+
semantics: TurnMessageSemantics;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const DEFAULT_RUNTIME_CAPABILITIES: RuntimeCapabilities;
|
|
39
|
+
export declare function normalizeTurnMetadata(metadata: unknown): TurnMetadata | null;
|
|
40
|
+
export declare function normalizeTurnState(value: unknown): TurnState | null;
|
|
41
|
+
export declare function isTurnOpen(turnState: Pick<TurnState, 'state'> | null | undefined): boolean;
|
|
42
|
+
export declare function resolveTurnMessageSemantics(input: {
|
|
43
|
+
senderType: 'human' | 'ai_agent';
|
|
44
|
+
metadata?: unknown;
|
|
45
|
+
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
46
|
+
}): TurnMessageSemantics;
|
|
47
|
+
export declare function shouldPromoteConversationMessage(input: {
|
|
48
|
+
senderType: 'human' | 'ai_agent';
|
|
49
|
+
metadata?: unknown;
|
|
50
|
+
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
51
|
+
}): boolean;
|
|
52
|
+
export declare function shouldTriggerAgentTurn(input: {
|
|
53
|
+
senderType: 'human' | 'ai_agent';
|
|
54
|
+
metadata?: unknown;
|
|
55
|
+
senderTurnState?: Pick<TurnState, 'state'> | null;
|
|
56
|
+
}): TriggerDecision;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export const FINAL_MESSAGE_HANDOFF_MS = 750;
|
|
2
|
+
const TURN_STATES = [
|
|
3
|
+
'idle',
|
|
4
|
+
'thinking',
|
|
5
|
+
'streaming',
|
|
6
|
+
'tool',
|
|
7
|
+
'waiting_input',
|
|
8
|
+
'completed',
|
|
9
|
+
'interrupted',
|
|
10
|
+
];
|
|
11
|
+
const TURN_SEMANTICS = [
|
|
12
|
+
'progress',
|
|
13
|
+
'turn_complete',
|
|
14
|
+
'control',
|
|
15
|
+
];
|
|
16
|
+
const DELIVERY_INTENTS = [
|
|
17
|
+
'queue',
|
|
18
|
+
'interrupt',
|
|
19
|
+
'interleave',
|
|
20
|
+
'stop',
|
|
21
|
+
];
|
|
22
|
+
export const DEFAULT_RUNTIME_CAPABILITIES = {
|
|
23
|
+
supportsInterrupt: false,
|
|
24
|
+
supportsQueue: true,
|
|
25
|
+
supportsInterleave: false,
|
|
26
|
+
supportsRequiresAction: false,
|
|
27
|
+
supportsNonFinalPermanentMessages: false,
|
|
28
|
+
};
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
export function normalizeTurnMetadata(metadata) {
|
|
33
|
+
if (!isRecord(metadata))
|
|
34
|
+
return null;
|
|
35
|
+
const turnId = typeof metadata.turnId === 'string' && metadata.turnId.trim()
|
|
36
|
+
? metadata.turnId.trim()
|
|
37
|
+
: null;
|
|
38
|
+
const turnSemantics = TURN_SEMANTICS.includes(metadata.turnSemantics)
|
|
39
|
+
? metadata.turnSemantics
|
|
40
|
+
: undefined;
|
|
41
|
+
const deliveryIntent = DELIVERY_INTENTS.includes(metadata.deliveryIntent)
|
|
42
|
+
? metadata.deliveryIntent
|
|
43
|
+
: undefined;
|
|
44
|
+
const replyBehavior = metadata.replyBehavior === 'allow_auto_reply' || metadata.replyBehavior === 'suppress_auto_reply'
|
|
45
|
+
? metadata.replyBehavior
|
|
46
|
+
: undefined;
|
|
47
|
+
const inboundDisposition = metadata.inboundDisposition === 'queued'
|
|
48
|
+
|| metadata.inboundDisposition === 'accepted_now'
|
|
49
|
+
|| metadata.inboundDisposition === 'interleaved'
|
|
50
|
+
|| metadata.inboundDisposition === 'trigger_suppressed'
|
|
51
|
+
|| metadata.inboundDisposition === 'rejected'
|
|
52
|
+
? metadata.inboundDisposition
|
|
53
|
+
: undefined;
|
|
54
|
+
if (!turnId && !turnSemantics && !deliveryIntent && typeof metadata.turnComplete !== 'boolean'
|
|
55
|
+
&& !replyBehavior && !inboundDisposition) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...(turnId ? { turnId } : {}),
|
|
60
|
+
...(turnSemantics ? { turnSemantics } : {}),
|
|
61
|
+
...(deliveryIntent ? { deliveryIntent } : {}),
|
|
62
|
+
...(typeof metadata.turnComplete === 'boolean' ? { turnComplete: metadata.turnComplete } : {}),
|
|
63
|
+
...(replyBehavior ? { replyBehavior } : {}),
|
|
64
|
+
...(inboundDisposition ? { inboundDisposition } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function normalizeTurnState(value) {
|
|
68
|
+
if (!isRecord(value))
|
|
69
|
+
return null;
|
|
70
|
+
const state = TURN_STATES.includes(value.state)
|
|
71
|
+
? value.state
|
|
72
|
+
: null;
|
|
73
|
+
if (!state)
|
|
74
|
+
return null;
|
|
75
|
+
return {
|
|
76
|
+
state,
|
|
77
|
+
queueDepth: typeof value.queueDepth === 'number' ? value.queueDepth : 0,
|
|
78
|
+
turnId: typeof value.turnId === 'string' ? value.turnId : null,
|
|
79
|
+
currentSpeakerId: typeof value.currentSpeakerId === 'string' ? value.currentSpeakerId : null,
|
|
80
|
+
lastAcceptedIntent: DELIVERY_INTENTS.includes(value.lastAcceptedIntent)
|
|
81
|
+
? value.lastAcceptedIntent
|
|
82
|
+
: null,
|
|
83
|
+
activeMessageIds: Array.isArray(value.activeMessageIds)
|
|
84
|
+
? value.activeMessageIds.filter((entry) => typeof entry === 'string')
|
|
85
|
+
: [],
|
|
86
|
+
capabilities: isRecord(value.capabilities)
|
|
87
|
+
? {
|
|
88
|
+
supportsInterrupt: Boolean(value.capabilities.supportsInterrupt),
|
|
89
|
+
supportsQueue: value.capabilities.supportsQueue !== false,
|
|
90
|
+
supportsInterleave: Boolean(value.capabilities.supportsInterleave),
|
|
91
|
+
supportsRequiresAction: Boolean(value.capabilities.supportsRequiresAction),
|
|
92
|
+
supportsNonFinalPermanentMessages: Boolean(value.capabilities.supportsNonFinalPermanentMessages),
|
|
93
|
+
}
|
|
94
|
+
: undefined,
|
|
95
|
+
openedAt: typeof value.openedAt === 'number' ? value.openedAt : undefined,
|
|
96
|
+
updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : undefined,
|
|
97
|
+
completedAt: typeof value.completedAt === 'number' ? value.completedAt : null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function isTurnOpen(turnState) {
|
|
101
|
+
if (!turnState)
|
|
102
|
+
return false;
|
|
103
|
+
return turnState.state !== 'idle'
|
|
104
|
+
&& turnState.state !== 'completed'
|
|
105
|
+
&& turnState.state !== 'interrupted';
|
|
106
|
+
}
|
|
107
|
+
export function resolveTurnMessageSemantics(input) {
|
|
108
|
+
const turnMetadata = normalizeTurnMetadata(input.metadata);
|
|
109
|
+
if (turnMetadata?.turnSemantics) {
|
|
110
|
+
return turnMetadata.turnSemantics;
|
|
111
|
+
}
|
|
112
|
+
if (turnMetadata?.turnComplete === true) {
|
|
113
|
+
return 'turn_complete';
|
|
114
|
+
}
|
|
115
|
+
if (input.senderType === 'human') {
|
|
116
|
+
return 'turn_complete';
|
|
117
|
+
}
|
|
118
|
+
return isTurnOpen(input.senderTurnState) ? 'progress' : 'turn_complete';
|
|
119
|
+
}
|
|
120
|
+
export function shouldPromoteConversationMessage(input) {
|
|
121
|
+
return resolveTurnMessageSemantics(input) !== 'progress';
|
|
122
|
+
}
|
|
123
|
+
export function shouldTriggerAgentTurn(input) {
|
|
124
|
+
const semantics = resolveTurnMessageSemantics(input);
|
|
125
|
+
const turnMetadata = normalizeTurnMetadata(input.metadata);
|
|
126
|
+
if (turnMetadata?.replyBehavior === 'suppress_auto_reply') {
|
|
127
|
+
return {
|
|
128
|
+
allow: false,
|
|
129
|
+
semantics,
|
|
130
|
+
reason: 'metadata explicitly suppresses auto-reply',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (input.senderType === 'human') {
|
|
134
|
+
return {
|
|
135
|
+
allow: true,
|
|
136
|
+
semantics,
|
|
137
|
+
reason: 'human messages always remain triggerable',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (semantics === 'progress') {
|
|
141
|
+
return {
|
|
142
|
+
allow: false,
|
|
143
|
+
semantics,
|
|
144
|
+
reason: 'non-final agent progress does not trigger other agents',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
allow: true,
|
|
149
|
+
semantics,
|
|
150
|
+
reason: 'agent message is treated as turn-complete',
|
|
151
|
+
};
|
|
152
|
+
}
|