@canonmsg/backend-contracts 5.0.0 → 5.2.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/README.md +3 -1
- package/dist/accessPolicy.d.ts +9 -0
- package/dist/accessPolicy.js +12 -0
- package/dist/agentBehaviorPolicy.d.ts +86 -15
- package/dist/agentBehaviorPolicy.js +103 -17
- package/dist/agentSearch.d.ts +17 -0
- package/dist/agentSearch.js +58 -0
- package/dist/canon-verb-wire.schema.json +4 -2
- package/dist/canon-verbs.limits.json +2 -1
- package/dist/canon-verbs.schema.json +34 -0
- package/dist/cjs/accessPolicy.js +15 -0
- package/dist/cjs/agentBehaviorPolicy.js +104 -17
- package/dist/cjs/agentSearch.js +64 -0
- package/dist/cjs/contactSources.js +45 -0
- package/dist/cjs/firestoreValues.js +27 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/moderation.js +12 -0
- package/dist/cjs/runtimeCardFields.js +30 -1
- package/dist/cjs/selfContext.js +33 -0
- package/dist/cjs/verbContract.js +18 -2
- package/dist/cjs/verbSchemas.js +26 -0
- package/dist/cjs/verbWire.js +8 -0
- package/dist/contactSources.d.ts +31 -0
- package/dist/contactSources.js +41 -0
- package/dist/firestoreValues.d.ts +9 -0
- package/dist/firestoreValues.js +23 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/moderation.d.ts +7 -0
- package/dist/moderation.js +9 -0
- package/dist/runtimeCardFields.d.ts +14 -2
- package/dist/runtimeCardFields.js +29 -0
- package/dist/selfContext.d.ts +9 -0
- package/dist/selfContext.js +29 -0
- package/dist/verbContract.d.ts +35 -2
- package/dist/verbContract.js +17 -1
- package/dist/verbSchemas.d.ts +32 -0
- package/dist/verbSchemas.js +26 -0
- package/dist/verbWire.d.ts +2 -2
- package/dist/verbWire.js +8 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Node.js 18+. Ships both ESM and CommonJS builds — Cloud Functions consume the
|
|
|
16
16
|
|
|
17
17
|
## What is in it
|
|
18
18
|
|
|
19
|
-
**The verb contract.** `canon.verbs.v1` (`verbContract.ts`) is the plaintext *intent* schema —
|
|
19
|
+
**The verb contract.** `canon.verbs.v1` (`verbContract.ts`) is the plaintext *intent* schema — seventeen verbs, their input and result schemas, byte limits, and rate limits. `canon.verb-wire.v1` (`verbWire.ts`) is what actually crosses the network: a server-readable envelope plus a body that is either `{ encoding: 'json', value }` or `{ encoding: 'mls', … }`. `projectVerbIntentToWire` and `mergeVerbWireToIntent` are the two halves of that split, so bindings and the server never disagree about which fields are envelope and which are content.
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
import {
|
|
@@ -34,6 +34,8 @@ The JSON Schemas are also emitted as files for non-JavaScript consumers: `@canon
|
|
|
34
34
|
|
|
35
35
|
**Behavior policy and contact requests.** The participation evaluator the stream service runs before dispatching a turn, and the contact-request serializer both sides validate against.
|
|
36
36
|
|
|
37
|
+
**Server-side normalizers.** `readCanonicalPolicy` resolves a stored `inboundPolicy` / `groupJoinPolicy` field to the access triplet's three values, defaulting to `approval-required` rather than widening to `open`. `serializeSelfContext` is the agent-facing projection of a stored self-context. `readModerationStatus` is the single definition of an ejected account. All take plain document-shaped data — the package still performs no I/O.
|
|
38
|
+
|
|
37
39
|
**Environments.** `CANON_ENVIRONMENT_CONTRACTS` binds `canon-dev-v1` and `canon-prod-v1` to their project and region; `getCanonEnvironmentContract` rejects anything else rather than defaulting.
|
|
38
40
|
|
|
39
41
|
**Diff redaction.** Approval diffs carry pre-image context into a message every conversation member can read, so secret-shaped paths and tokens are suppressed before send — by the emitting host, and again defensively by the server.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The canonical access triplet's policy values (`inboundPolicy`, `groupJoinPolicy`). */
|
|
2
|
+
export type CanonicalPolicy = 'open' | 'approval-required' | 'owner-only';
|
|
3
|
+
/**
|
|
4
|
+
* Read a stored policy field, defaulting to the value Canon assigns on account
|
|
5
|
+
* creation. Anything unrecognized — missing, misspelled, or the retired
|
|
6
|
+
* `'private'` enum — reads as 'approval-required'. This must never widen to
|
|
7
|
+
* 'open'.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readCanonicalPolicy(value: unknown): CanonicalPolicy;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a stored policy field, defaulting to the value Canon assigns on account
|
|
3
|
+
* creation. Anything unrecognized — missing, misspelled, or the retired
|
|
4
|
+
* `'private'` enum — reads as 'approval-required'. This must never widen to
|
|
5
|
+
* 'open'.
|
|
6
|
+
*/
|
|
7
|
+
export function readCanonicalPolicy(value) {
|
|
8
|
+
if (value === 'open' || value === 'approval-required' || value === 'owner-only') {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
return 'approval-required';
|
|
12
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
/** Conversation scope a policy is resolved for. Absent means "agent scope". */
|
|
2
|
+
export type PolicyConversationType = 'direct' | 'group' | 'unknown';
|
|
1
3
|
export type ParticipationStyle = 'natural' | 'collaborative' | 'mention-first' | 'approval-gated' | 'handoff-only' | 'observer';
|
|
4
|
+
/**
|
|
5
|
+
* The stored/parsed shape. This is the RETURN type of
|
|
6
|
+
* {@link parseAgentBehaviorSettings} and {@link normalizeStoredAgentBehaviorPolicy},
|
|
7
|
+
* so its optional fields deliberately stay `T | undefined` — widening them to
|
|
8
|
+
* `T | null` would be a breaking change for every reader of those results.
|
|
9
|
+
* Parameter positions take {@link AgentBehaviorSettingsInput} instead.
|
|
10
|
+
*/
|
|
2
11
|
export interface AgentBehaviorSettingsRecord {
|
|
3
12
|
participationStyle?: ParticipationStyle;
|
|
4
13
|
allowAgentToAgent?: boolean;
|
|
@@ -7,15 +16,30 @@ export interface AgentBehaviorSettingsRecord {
|
|
|
7
16
|
maxConsecutiveAgentTurns?: number | null;
|
|
8
17
|
instructions?: string | null;
|
|
9
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The input shape accepted by {@link resolveAgentBehaviorPolicy}. Every field
|
|
21
|
+
* additionally accepts an explicit `null` (an editor clearing a value), which
|
|
22
|
+
* coalesces exactly like `undefined` — the field falls through to the next
|
|
23
|
+
* layer. {@link AgentBehaviorSettingsRecord} is assignable to this type.
|
|
24
|
+
*/
|
|
25
|
+
export interface AgentBehaviorSettingsInput {
|
|
26
|
+
participationStyle?: ParticipationStyle | null;
|
|
27
|
+
allowAgentToAgent?: boolean | null;
|
|
28
|
+
allowLongRunningCollaboration?: boolean | null;
|
|
29
|
+
requireMentionForGroupReplies?: boolean | null;
|
|
30
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
31
|
+
instructions?: string | null;
|
|
32
|
+
}
|
|
33
|
+
export interface ParticipationPolicy {
|
|
34
|
+
style: ParticipationStyle;
|
|
35
|
+
allowAgentToAgent: boolean;
|
|
36
|
+
allowHumanToAgent: boolean;
|
|
37
|
+
allowLongRunningCollaboration: boolean;
|
|
38
|
+
requireMentionForGroupReplies: boolean;
|
|
39
|
+
maxConsecutiveAgentTurns?: number | null;
|
|
40
|
+
}
|
|
10
41
|
export interface ResolvedAgentBehaviorPolicyRecord {
|
|
11
|
-
participation:
|
|
12
|
-
style: ParticipationStyle;
|
|
13
|
-
allowAgentToAgent: boolean;
|
|
14
|
-
allowHumanToAgent: boolean;
|
|
15
|
-
allowLongRunningCollaboration: boolean;
|
|
16
|
-
requireMentionForGroupReplies: boolean;
|
|
17
|
-
maxConsecutiveAgentTurns?: number | null;
|
|
18
|
-
};
|
|
42
|
+
participation: ParticipationPolicy;
|
|
19
43
|
instructions: string[];
|
|
20
44
|
source: {
|
|
21
45
|
hasAgentDefault: boolean;
|
|
@@ -23,15 +47,27 @@ export interface ResolvedAgentBehaviorPolicyRecord {
|
|
|
23
47
|
};
|
|
24
48
|
}
|
|
25
49
|
export interface ParticipationDecisionInput {
|
|
26
|
-
conversationType:
|
|
50
|
+
conversationType: PolicyConversationType;
|
|
27
51
|
senderType: 'human' | 'ai_agent';
|
|
28
52
|
isOwner: boolean;
|
|
29
53
|
mentionedAgent: boolean;
|
|
30
54
|
recentHumanCount?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Consecutive agent turns already in the streak, NOT counting the message
|
|
57
|
+
* being evaluated. The message itself adds one more when its sender is an
|
|
58
|
+
* agent, and a delivery would add a further one — see the turn-cap check in
|
|
59
|
+
* {@link evaluateParticipationPolicy}.
|
|
60
|
+
*/
|
|
31
61
|
consecutiveAgentTurns?: number;
|
|
32
62
|
currentAgentStreakStartedByHuman?: boolean;
|
|
33
63
|
}
|
|
64
|
+
export interface ParticipationDecision {
|
|
65
|
+
allow: boolean;
|
|
66
|
+
reasonCode: string;
|
|
67
|
+
reason: string;
|
|
68
|
+
}
|
|
34
69
|
export interface ParticipationHistoryMessage {
|
|
70
|
+
id?: string;
|
|
35
71
|
senderId: string;
|
|
36
72
|
senderType: 'human' | 'ai_agent';
|
|
37
73
|
metadata?: unknown;
|
|
@@ -44,16 +80,51 @@ export interface ParticipationHistorySnapshot {
|
|
|
44
80
|
currentAgentStreakStartedByHuman: boolean;
|
|
45
81
|
}
|
|
46
82
|
export declare const PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
83
|
+
/**
|
|
84
|
+
* Safety backstop for groups with no explicit turn cap: at most this many
|
|
85
|
+
* consecutive agent turns before a human has to speak again.
|
|
86
|
+
*
|
|
87
|
+
* This is a backstop, not the steering wheel — agents are expected to stop on
|
|
88
|
+
* their own. Direct conversations deliberately stay unlimited: only a non-agent
|
|
89
|
+
* message resets the streak, so a default cap in a pure agent-to-agent DM would
|
|
90
|
+
* halt the room permanently instead of merely bounding a loop.
|
|
91
|
+
*
|
|
92
|
+
* The same caveat applies to the one group shape this default does cover: a
|
|
93
|
+
* group whose members are all agents (reachable — agent-created groups need no
|
|
94
|
+
* human member) has nothing that resets the streak, so it stops auto-replying
|
|
95
|
+
* for good once the cap is reached. That is deliberate: an unattended room is
|
|
96
|
+
* where a runaway loop is most expensive. Recovery needs a human member — the
|
|
97
|
+
* per-conversation `null` opt-out is writable only by a group owner or admin,
|
|
98
|
+
* who must be a member. Teaching the resolver "does this room contain a
|
|
99
|
+
* human?" would mean loading member types at every resolution site, including
|
|
100
|
+
* the stream gate, which does not read them today.
|
|
101
|
+
*/
|
|
102
|
+
export declare const DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS = 4;
|
|
47
103
|
export declare function parseAgentBehaviorSettings(raw: unknown): AgentBehaviorSettingsRecord;
|
|
48
104
|
export declare function normalizeStoredAgentBehaviorPolicy(raw: Record<string, unknown> | undefined): AgentBehaviorSettingsRecord | null;
|
|
49
105
|
export declare function normalizeAgentBehaviorInstructions(value: string | null | undefined): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* The scope-independent participation defaults. `maxConsecutiveAgentTurns` is
|
|
108
|
+
* `null` here because the turn cap is scope-dependent — a group with no stored
|
|
109
|
+
* setting resolves to {@link DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS}. Use
|
|
110
|
+
* {@link resolveAgentBehaviorPolicy} with a `conversationType` to learn what a
|
|
111
|
+
* given conversation actually enforces.
|
|
112
|
+
*/
|
|
113
|
+
export declare function getDefaultParticipationPolicy(): ParticipationPolicy;
|
|
114
|
+
/**
|
|
115
|
+
* Coalesce agent defaults and a conversation override into the resolved record
|
|
116
|
+
* every runtime reads. `conversationType` selects the Canon-wide fallbacks that
|
|
117
|
+
* differ per scope — today only the turn cap, which defaults to
|
|
118
|
+
* {@link DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS} in groups and to unlimited
|
|
119
|
+
* everywhere else. Omit it when resolving for the agent scope (no conversation);
|
|
120
|
+
* an absent or unknown type always resolves to the unlimited default, so a call
|
|
121
|
+
* site that forgets to thread it can never invent a cap.
|
|
122
|
+
*/
|
|
50
123
|
export declare function resolveAgentBehaviorPolicy(params?: {
|
|
51
|
-
agentDefault?:
|
|
52
|
-
conversationOverride?:
|
|
124
|
+
agentDefault?: AgentBehaviorSettingsInput | null;
|
|
125
|
+
conversationOverride?: AgentBehaviorSettingsInput | null;
|
|
126
|
+
conversationType?: PolicyConversationType;
|
|
53
127
|
}): ResolvedAgentBehaviorPolicyRecord;
|
|
54
128
|
export declare function buildParticipationHistorySnapshot(messages: ParticipationHistoryMessage[], agentId?: string): ParticipationHistorySnapshot;
|
|
55
129
|
export declare function appendParticipationHistoryMessage(snapshot: ParticipationHistorySnapshot, message: ParticipationHistoryMessage, limit?: number): ParticipationHistorySnapshot;
|
|
56
|
-
export declare function evaluateParticipationPolicy(policy: ResolvedAgentBehaviorPolicyRecord, input: ParticipationDecisionInput):
|
|
57
|
-
allow: boolean;
|
|
58
|
-
reason: string;
|
|
59
|
-
};
|
|
130
|
+
export declare function evaluateParticipationPolicy(policy: ResolvedAgentBehaviorPolicyRecord | null | undefined, input: ParticipationDecisionInput): ParticipationDecision;
|
|
@@ -1,5 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { shouldTriggerAgentTurn } from './turnProtocol.js';
|
|
2
2
|
export const PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
3
|
+
/**
|
|
4
|
+
* Safety backstop for groups with no explicit turn cap: at most this many
|
|
5
|
+
* consecutive agent turns before a human has to speak again.
|
|
6
|
+
*
|
|
7
|
+
* This is a backstop, not the steering wheel — agents are expected to stop on
|
|
8
|
+
* their own. Direct conversations deliberately stay unlimited: only a non-agent
|
|
9
|
+
* message resets the streak, so a default cap in a pure agent-to-agent DM would
|
|
10
|
+
* halt the room permanently instead of merely bounding a loop.
|
|
11
|
+
*
|
|
12
|
+
* The same caveat applies to the one group shape this default does cover: a
|
|
13
|
+
* group whose members are all agents (reachable — agent-created groups need no
|
|
14
|
+
* human member) has nothing that resets the streak, so it stops auto-replying
|
|
15
|
+
* for good once the cap is reached. That is deliberate: an unattended room is
|
|
16
|
+
* where a runaway loop is most expensive. Recovery needs a human member — the
|
|
17
|
+
* per-conversation `null` opt-out is writable only by a group owner or admin,
|
|
18
|
+
* who must be a member. Teaching the resolver "does this room contain a
|
|
19
|
+
* human?" would mean loading member types at every resolution site, including
|
|
20
|
+
* the stream gate, which does not read them today.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS = 4;
|
|
23
|
+
function defaultMaxConsecutiveAgentTurns(conversationType) {
|
|
24
|
+
return conversationType === 'group'
|
|
25
|
+
? DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
3
28
|
const VALID_PARTICIPATION_STYLES = new Set([
|
|
4
29
|
'natural',
|
|
5
30
|
'collaborative',
|
|
@@ -119,6 +144,25 @@ export function normalizeAgentBehaviorInstructions(value) {
|
|
|
119
144
|
const trimmed = value.trim();
|
|
120
145
|
return trimmed.length > 0 ? trimmed : null;
|
|
121
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The scope-independent participation defaults. `maxConsecutiveAgentTurns` is
|
|
149
|
+
* `null` here because the turn cap is scope-dependent — a group with no stored
|
|
150
|
+
* setting resolves to {@link DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS}. Use
|
|
151
|
+
* {@link resolveAgentBehaviorPolicy} with a `conversationType` to learn what a
|
|
152
|
+
* given conversation actually enforces.
|
|
153
|
+
*/
|
|
154
|
+
export function getDefaultParticipationPolicy() {
|
|
155
|
+
return { ...DEFAULT_POLICY.participation };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Coalesce agent defaults and a conversation override into the resolved record
|
|
159
|
+
* every runtime reads. `conversationType` selects the Canon-wide fallbacks that
|
|
160
|
+
* differ per scope — today only the turn cap, which defaults to
|
|
161
|
+
* {@link DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS} in groups and to unlimited
|
|
162
|
+
* everywhere else. Omit it when resolving for the agent scope (no conversation);
|
|
163
|
+
* an absent or unknown type always resolves to the unlimited default, so a call
|
|
164
|
+
* site that forgets to thread it can never invent a cap.
|
|
165
|
+
*/
|
|
122
166
|
export function resolveAgentBehaviorPolicy(params) {
|
|
123
167
|
const agentDefault = params?.agentDefault ?? null;
|
|
124
168
|
const conversationOverride = params?.conversationOverride ?? null;
|
|
@@ -137,7 +181,7 @@ export function resolveAgentBehaviorPolicy(params) {
|
|
|
137
181
|
? conversationOverride.maxConsecutiveAgentTurns ?? null
|
|
138
182
|
: agentDefault?.maxConsecutiveAgentTurns !== undefined
|
|
139
183
|
? agentDefault.maxConsecutiveAgentTurns ?? null
|
|
140
|
-
:
|
|
184
|
+
: defaultMaxConsecutiveAgentTurns(params?.conversationType),
|
|
141
185
|
},
|
|
142
186
|
instructions: [
|
|
143
187
|
...(defaultInstructions ? [defaultInstructions] : []),
|
|
@@ -149,14 +193,22 @@ export function resolveAgentBehaviorPolicy(params) {
|
|
|
149
193
|
},
|
|
150
194
|
};
|
|
151
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* The streak counts exactly the agent messages that can trigger another agent —
|
|
198
|
+
* the only messages a loop can be built from. Reusing the trigger gate keeps the
|
|
199
|
+
* two in lockstep: it admits `control` messages that do not suppress auto-reply
|
|
200
|
+
* (hand-crafted runtime metadata can otherwise drive an uncounted loop) and
|
|
201
|
+
* still drops chunk parts, interim progress, card round-trips and media/failure
|
|
202
|
+
* notices, none of which trigger anyone and none of which should burn a slot.
|
|
203
|
+
*/
|
|
152
204
|
function toHistorySenderType(message) {
|
|
153
205
|
if (message.senderType !== 'ai_agent') {
|
|
154
206
|
return 'human';
|
|
155
207
|
}
|
|
156
|
-
return
|
|
157
|
-
senderType:
|
|
208
|
+
return shouldTriggerAgentTurn({
|
|
209
|
+
senderType: 'ai_agent',
|
|
158
210
|
metadata: message.metadata,
|
|
159
|
-
})
|
|
211
|
+
}).allow
|
|
160
212
|
? 'ai_agent'
|
|
161
213
|
: null;
|
|
162
214
|
}
|
|
@@ -195,37 +247,71 @@ export function appendParticipationHistoryMessage(snapshot, message, limit = PAR
|
|
|
195
247
|
].slice(0, limit));
|
|
196
248
|
}
|
|
197
249
|
export function evaluateParticipationPolicy(policy, input) {
|
|
198
|
-
const
|
|
250
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy({ conversationType: input.conversationType });
|
|
251
|
+
const participation = resolved.participation;
|
|
252
|
+
const priorAgentTurns = Math.max(input.consecutiveAgentTurns ?? 0, 0);
|
|
253
|
+
/** Streak length once the message being evaluated is counted. */
|
|
254
|
+
const agentTurnsWithCurrent = priorAgentTurns
|
|
255
|
+
+ (input.senderType === 'ai_agent' ? 1 : 0);
|
|
256
|
+
/**
|
|
257
|
+
* The long-running-collaboration brake reads "more than the current turn",
|
|
258
|
+
* so it keeps its historical floor rather than the exact streak length.
|
|
259
|
+
*/
|
|
260
|
+
const consecutiveAgentTurns = Math.max(priorAgentTurns, input.senderType === 'ai_agent' ? 1 : 0);
|
|
199
261
|
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
200
262
|
if (input.conversationType === 'group'
|
|
201
|
-
&&
|
|
263
|
+
&& participation.requireMentionForGroupReplies
|
|
202
264
|
&& !input.mentionedAgent) {
|
|
203
265
|
return {
|
|
204
266
|
allow: false,
|
|
267
|
+
reasonCode: 'group_mention_required',
|
|
205
268
|
reason: 'group replies require a direct mention',
|
|
206
269
|
};
|
|
207
270
|
}
|
|
208
271
|
if (input.isOwner) {
|
|
209
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
allow: true,
|
|
274
|
+
reasonCode: 'owner_sender',
|
|
275
|
+
reason: 'owner messages pass through outside mention-required group turns',
|
|
276
|
+
};
|
|
210
277
|
}
|
|
211
278
|
if (input.senderType !== 'ai_agent') {
|
|
212
|
-
return { allow: true, reason: 'latest sender is human' };
|
|
279
|
+
return { allow: true, reasonCode: 'human_sender', reason: 'latest sender is human' };
|
|
213
280
|
}
|
|
214
|
-
if (!
|
|
215
|
-
return {
|
|
281
|
+
if (!participation.allowAgentToAgent) {
|
|
282
|
+
return {
|
|
283
|
+
allow: false,
|
|
284
|
+
reasonCode: 'agent_to_agent_disabled',
|
|
285
|
+
reason: 'agent-to-agent participation is disabled by policy',
|
|
286
|
+
};
|
|
216
287
|
}
|
|
217
|
-
if (!
|
|
288
|
+
if (!participation.allowLongRunningCollaboration
|
|
218
289
|
&& (consecutiveAgentTurns > 1
|
|
219
290
|
|| !currentAgentStreakStartedByHuman)) {
|
|
220
291
|
return {
|
|
221
292
|
allow: false,
|
|
293
|
+
reasonCode: 'human_reset_required',
|
|
222
294
|
reason: 'a fresh human steer is required before continuing agent collaboration',
|
|
223
295
|
};
|
|
224
296
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
297
|
+
// `maxConsecutiveAgentTurns: N` means at most N consecutive agent turns
|
|
298
|
+
// without a human in between. Delivering this message triggers turn
|
|
299
|
+
// `agentTurnsWithCurrent + 1`, so it is suppressed as soon as the streak has
|
|
300
|
+
// already reached N.
|
|
301
|
+
if (typeof participation.maxConsecutiveAgentTurns === 'number'
|
|
302
|
+
&& participation.maxConsecutiveAgentTurns >= 0
|
|
303
|
+
&& agentTurnsWithCurrent >= participation.maxConsecutiveAgentTurns) {
|
|
304
|
+
return {
|
|
305
|
+
allow: false,
|
|
306
|
+
reasonCode: 'agent_turn_limit_reached',
|
|
307
|
+
reason: 'maximum consecutive agent turns reached',
|
|
308
|
+
};
|
|
229
309
|
}
|
|
230
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
allow: true,
|
|
312
|
+
reasonCode: input.conversationType === 'group' ? 'group_agent_allowed' : 'direct_agent_allowed',
|
|
313
|
+
reason: participation.requireMentionForGroupReplies
|
|
314
|
+
? 'policy allows this directly mentioned group reply'
|
|
315
|
+
: 'agent-to-agent participation allowed by policy',
|
|
316
|
+
};
|
|
231
317
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The indexable fields a stored agent contributes to `searchPrefixes`. */
|
|
2
|
+
export interface AgentSearchIndexFields {
|
|
3
|
+
displayName?: string | null;
|
|
4
|
+
description?: string | null;
|
|
5
|
+
}
|
|
6
|
+
/** The agent shape a directory filter reads. */
|
|
7
|
+
export interface AgentSearchProfile {
|
|
8
|
+
displayName?: string | null;
|
|
9
|
+
agentConfig?: {
|
|
10
|
+
description?: string | null;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
/** Tolerant: non-string input normalises to ''. */
|
|
14
|
+
export declare function normalizeAgentDirectoryQuery(value: unknown): string;
|
|
15
|
+
export declare function getAgentDirectoryQueryTokens(query: string): string[];
|
|
16
|
+
export declare function buildAgentSearchPrefixes(input: AgentSearchIndexFields): string[];
|
|
17
|
+
export declare function matchesAgentSearchQuery(profile: AgentSearchProfile, query: string): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The single agent-directory search implementation, shared by Cloud Functions
|
|
2
|
+
// (which writes `users/{agentId}.searchPrefixes` and filters discovery reads)
|
|
3
|
+
// and `@canonmsg/chat-domain` (which filters the same agents client-side in the
|
|
4
|
+
// app and on web). The server index and the client filter must agree, so both
|
|
5
|
+
// sides derive from these bodies rather than from hand-synced copies.
|
|
6
|
+
//
|
|
7
|
+
// Pure ECMAScript: no imports, no Node built-ins, no Firestore types — safe in
|
|
8
|
+
// CJS, in the browser bundle, and on Hermes.
|
|
9
|
+
const MAX_QUERY_LENGTH = 64;
|
|
10
|
+
const MAX_TOKEN_PREFIX_LENGTH = 32;
|
|
11
|
+
const MAX_SEARCH_PREFIXES = 256;
|
|
12
|
+
const TOKEN_PATTERN = /[\p{L}\p{N}]+/gu;
|
|
13
|
+
function tokenizeAgentSearchValue(value) {
|
|
14
|
+
const normalized = value
|
|
15
|
+
.normalize('NFKD')
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.trim();
|
|
18
|
+
return normalized.match(TOKEN_PATTERN) ?? [];
|
|
19
|
+
}
|
|
20
|
+
/** Tolerant: non-string input normalises to ''. */
|
|
21
|
+
export function normalizeAgentDirectoryQuery(value) {
|
|
22
|
+
if (typeof value !== 'string')
|
|
23
|
+
return '';
|
|
24
|
+
return tokenizeAgentSearchValue(value).join(' ').slice(0, MAX_QUERY_LENGTH);
|
|
25
|
+
}
|
|
26
|
+
export function getAgentDirectoryQueryTokens(query) {
|
|
27
|
+
return tokenizeAgentSearchValue(query).map((token) => token.slice(0, MAX_QUERY_LENGTH));
|
|
28
|
+
}
|
|
29
|
+
export function buildAgentSearchPrefixes(input) {
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const token of tokenizeAgentSearchValue([input.displayName, input.description].filter(Boolean).join(' '))) {
|
|
32
|
+
const normalizedToken = token.slice(0, MAX_QUERY_LENGTH);
|
|
33
|
+
const prefixLength = Math.min(normalizedToken.length, MAX_TOKEN_PREFIX_LENGTH);
|
|
34
|
+
for (let index = 1; index <= prefixLength; index += 1) {
|
|
35
|
+
seen.add(normalizedToken.slice(0, index));
|
|
36
|
+
if (seen.size >= MAX_SEARCH_PREFIXES) {
|
|
37
|
+
return [...seen];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (normalizedToken.length > MAX_TOKEN_PREFIX_LENGTH) {
|
|
41
|
+
seen.add(normalizedToken);
|
|
42
|
+
if (seen.size >= MAX_SEARCH_PREFIXES) {
|
|
43
|
+
return [...seen];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [...seen];
|
|
48
|
+
}
|
|
49
|
+
export function matchesAgentSearchQuery(profile, query) {
|
|
50
|
+
const tokens = getAgentDirectoryQueryTokens(query);
|
|
51
|
+
if (tokens.length === 0)
|
|
52
|
+
return true;
|
|
53
|
+
const prefixes = new Set(buildAgentSearchPrefixes({
|
|
54
|
+
displayName: profile.displayName,
|
|
55
|
+
description: profile.agentConfig?.description,
|
|
56
|
+
}));
|
|
57
|
+
return tokens.every((token) => prefixes.has(token));
|
|
58
|
+
}
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"leave_conversation",
|
|
33
33
|
"list_contacts",
|
|
34
34
|
"list_contact_requests",
|
|
35
|
-
"list_conversations"
|
|
35
|
+
"list_conversations",
|
|
36
|
+
"no_reply"
|
|
36
37
|
]
|
|
37
38
|
},
|
|
38
39
|
"envelope": {
|
|
@@ -506,7 +507,8 @@
|
|
|
506
507
|
"leave_conversation",
|
|
507
508
|
"list_contacts",
|
|
508
509
|
"list_contact_requests",
|
|
509
|
-
"list_conversations"
|
|
510
|
+
"list_conversations",
|
|
511
|
+
"no_reply"
|
|
510
512
|
]
|
|
511
513
|
},
|
|
512
514
|
"result": {
|
|
@@ -1891,6 +1891,40 @@
|
|
|
1891
1891
|
}
|
|
1892
1892
|
}
|
|
1893
1893
|
}
|
|
1894
|
+
},
|
|
1895
|
+
"no_reply_input": {
|
|
1896
|
+
"type": "object",
|
|
1897
|
+
"description": "End this turn without posting anything to the conversation. Nothing is rendered and no other member or agent is triggered.",
|
|
1898
|
+
"additionalProperties": false,
|
|
1899
|
+
"properties": {
|
|
1900
|
+
"conversationId": {
|
|
1901
|
+
"type": "string",
|
|
1902
|
+
"pattern": "^[A-Za-z0-9_.:-]{1,160}$"
|
|
1903
|
+
},
|
|
1904
|
+
"reason": {
|
|
1905
|
+
"type": "string",
|
|
1906
|
+
"maxLength": 500,
|
|
1907
|
+
"description": "Never rendered; logged only."
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
"no_reply_result": {
|
|
1912
|
+
"type": "object",
|
|
1913
|
+
"required": [
|
|
1914
|
+
"status"
|
|
1915
|
+
],
|
|
1916
|
+
"additionalProperties": true,
|
|
1917
|
+
"properties": {
|
|
1918
|
+
"status": {
|
|
1919
|
+
"const": "acknowledged"
|
|
1920
|
+
},
|
|
1921
|
+
"conversationId": {
|
|
1922
|
+
"type": "string"
|
|
1923
|
+
},
|
|
1924
|
+
"note": {
|
|
1925
|
+
"type": "string"
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1894
1928
|
}
|
|
1895
1929
|
}
|
|
1896
1930
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readCanonicalPolicy = readCanonicalPolicy;
|
|
4
|
+
/**
|
|
5
|
+
* Read a stored policy field, defaulting to the value Canon assigns on account
|
|
6
|
+
* creation. Anything unrecognized — missing, misspelled, or the retired
|
|
7
|
+
* `'private'` enum — reads as 'approval-required'. This must never widen to
|
|
8
|
+
* 'open'.
|
|
9
|
+
*/
|
|
10
|
+
function readCanonicalPolicy(value) {
|
|
11
|
+
if (value === 'open' || value === 'approval-required' || value === 'owner-only') {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
return 'approval-required';
|
|
15
|
+
}
|