@canonmsg/backend-contracts 5.1.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 +1 -1
- package/dist/agentBehaviorPolicy.d.ts +46 -1
- package/dist/agentBehaviorPolicy.js +69 -8
- 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/agentBehaviorPolicy.js +69 -8
- package/dist/cjs/verbContract.js +18 -2
- package/dist/cjs/verbSchemas.js +26 -0
- package/dist/cjs/verbWire.js +8 -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 {
|
|
@@ -1,3 +1,5 @@
|
|
|
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';
|
|
2
4
|
/**
|
|
3
5
|
* The stored/parsed shape. This is the RETURN type of
|
|
@@ -45,11 +47,17 @@ export interface ResolvedAgentBehaviorPolicyRecord {
|
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
export interface ParticipationDecisionInput {
|
|
48
|
-
conversationType:
|
|
50
|
+
conversationType: PolicyConversationType;
|
|
49
51
|
senderType: 'human' | 'ai_agent';
|
|
50
52
|
isOwner: boolean;
|
|
51
53
|
mentionedAgent: boolean;
|
|
52
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
|
+
*/
|
|
53
61
|
consecutiveAgentTurns?: number;
|
|
54
62
|
currentAgentStreakStartedByHuman?: boolean;
|
|
55
63
|
}
|
|
@@ -72,13 +80,50 @@ export interface ParticipationHistorySnapshot {
|
|
|
72
80
|
currentAgentStreakStartedByHuman: boolean;
|
|
73
81
|
}
|
|
74
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;
|
|
75
103
|
export declare function parseAgentBehaviorSettings(raw: unknown): AgentBehaviorSettingsRecord;
|
|
76
104
|
export declare function normalizeStoredAgentBehaviorPolicy(raw: Record<string, unknown> | undefined): AgentBehaviorSettingsRecord | null;
|
|
77
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
|
+
*/
|
|
78
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
|
+
*/
|
|
79
123
|
export declare function resolveAgentBehaviorPolicy(params?: {
|
|
80
124
|
agentDefault?: AgentBehaviorSettingsInput | null;
|
|
81
125
|
conversationOverride?: AgentBehaviorSettingsInput | null;
|
|
126
|
+
conversationType?: PolicyConversationType;
|
|
82
127
|
}): ResolvedAgentBehaviorPolicyRecord;
|
|
83
128
|
export declare function buildParticipationHistorySnapshot(messages: ParticipationHistoryMessage[], agentId?: string): ParticipationHistorySnapshot;
|
|
84
129
|
export declare function appendParticipationHistoryMessage(snapshot: ParticipationHistorySnapshot, message: ParticipationHistoryMessage, limit?: number): ParticipationHistorySnapshot;
|
|
@@ -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,9 +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
|
+
*/
|
|
122
154
|
export function getDefaultParticipationPolicy() {
|
|
123
155
|
return { ...DEFAULT_POLICY.participation };
|
|
124
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
|
+
*/
|
|
125
166
|
export function resolveAgentBehaviorPolicy(params) {
|
|
126
167
|
const agentDefault = params?.agentDefault ?? null;
|
|
127
168
|
const conversationOverride = params?.conversationOverride ?? null;
|
|
@@ -140,7 +181,7 @@ export function resolveAgentBehaviorPolicy(params) {
|
|
|
140
181
|
? conversationOverride.maxConsecutiveAgentTurns ?? null
|
|
141
182
|
: agentDefault?.maxConsecutiveAgentTurns !== undefined
|
|
142
183
|
? agentDefault.maxConsecutiveAgentTurns ?? null
|
|
143
|
-
:
|
|
184
|
+
: defaultMaxConsecutiveAgentTurns(params?.conversationType),
|
|
144
185
|
},
|
|
145
186
|
instructions: [
|
|
146
187
|
...(defaultInstructions ? [defaultInstructions] : []),
|
|
@@ -152,14 +193,22 @@ export function resolveAgentBehaviorPolicy(params) {
|
|
|
152
193
|
},
|
|
153
194
|
};
|
|
154
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
|
+
*/
|
|
155
204
|
function toHistorySenderType(message) {
|
|
156
205
|
if (message.senderType !== 'ai_agent') {
|
|
157
206
|
return 'human';
|
|
158
207
|
}
|
|
159
|
-
return
|
|
160
|
-
senderType:
|
|
208
|
+
return shouldTriggerAgentTurn({
|
|
209
|
+
senderType: 'ai_agent',
|
|
161
210
|
metadata: message.metadata,
|
|
162
|
-
})
|
|
211
|
+
}).allow
|
|
163
212
|
? 'ai_agent'
|
|
164
213
|
: null;
|
|
165
214
|
}
|
|
@@ -198,9 +247,17 @@ export function appendParticipationHistoryMessage(snapshot, message, limit = PAR
|
|
|
198
247
|
].slice(0, limit));
|
|
199
248
|
}
|
|
200
249
|
export function evaluateParticipationPolicy(policy, input) {
|
|
201
|
-
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
250
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy({ conversationType: input.conversationType });
|
|
202
251
|
const participation = resolved.participation;
|
|
203
|
-
const
|
|
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);
|
|
204
261
|
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
205
262
|
if (input.conversationType === 'group'
|
|
206
263
|
&& participation.requireMentionForGroupReplies
|
|
@@ -237,9 +294,13 @@ export function evaluateParticipationPolicy(policy, input) {
|
|
|
237
294
|
reason: 'a fresh human steer is required before continuing agent collaboration',
|
|
238
295
|
};
|
|
239
296
|
}
|
|
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.
|
|
240
301
|
if (typeof participation.maxConsecutiveAgentTurns === 'number'
|
|
241
302
|
&& participation.maxConsecutiveAgentTurns >= 0
|
|
242
|
-
&&
|
|
303
|
+
&& agentTurnsWithCurrent >= participation.maxConsecutiveAgentTurns) {
|
|
243
304
|
return {
|
|
244
305
|
allow: false,
|
|
245
306
|
reasonCode: 'agent_turn_limit_reached',
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PARTICIPATION_HISTORY_FETCH_LIMIT = void 0;
|
|
3
|
+
exports.DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS = exports.PARTICIPATION_HISTORY_FETCH_LIMIT = void 0;
|
|
4
4
|
exports.parseAgentBehaviorSettings = parseAgentBehaviorSettings;
|
|
5
5
|
exports.normalizeStoredAgentBehaviorPolicy = normalizeStoredAgentBehaviorPolicy;
|
|
6
6
|
exports.normalizeAgentBehaviorInstructions = normalizeAgentBehaviorInstructions;
|
|
@@ -11,6 +11,31 @@ exports.appendParticipationHistoryMessage = appendParticipationHistoryMessage;
|
|
|
11
11
|
exports.evaluateParticipationPolicy = evaluateParticipationPolicy;
|
|
12
12
|
const turnProtocol_js_1 = require("./turnProtocol.js");
|
|
13
13
|
exports.PARTICIPATION_HISTORY_FETCH_LIMIT = 50;
|
|
14
|
+
/**
|
|
15
|
+
* Safety backstop for groups with no explicit turn cap: at most this many
|
|
16
|
+
* consecutive agent turns before a human has to speak again.
|
|
17
|
+
*
|
|
18
|
+
* This is a backstop, not the steering wheel — agents are expected to stop on
|
|
19
|
+
* their own. Direct conversations deliberately stay unlimited: only a non-agent
|
|
20
|
+
* message resets the streak, so a default cap in a pure agent-to-agent DM would
|
|
21
|
+
* halt the room permanently instead of merely bounding a loop.
|
|
22
|
+
*
|
|
23
|
+
* The same caveat applies to the one group shape this default does cover: a
|
|
24
|
+
* group whose members are all agents (reachable — agent-created groups need no
|
|
25
|
+
* human member) has nothing that resets the streak, so it stops auto-replying
|
|
26
|
+
* for good once the cap is reached. That is deliberate: an unattended room is
|
|
27
|
+
* where a runaway loop is most expensive. Recovery needs a human member — the
|
|
28
|
+
* per-conversation `null` opt-out is writable only by a group owner or admin,
|
|
29
|
+
* who must be a member. Teaching the resolver "does this room contain a
|
|
30
|
+
* human?" would mean loading member types at every resolution site, including
|
|
31
|
+
* the stream gate, which does not read them today.
|
|
32
|
+
*/
|
|
33
|
+
exports.DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS = 4;
|
|
34
|
+
function defaultMaxConsecutiveAgentTurns(conversationType) {
|
|
35
|
+
return conversationType === 'group'
|
|
36
|
+
? exports.DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS
|
|
37
|
+
: null;
|
|
38
|
+
}
|
|
14
39
|
const VALID_PARTICIPATION_STYLES = new Set([
|
|
15
40
|
'natural',
|
|
16
41
|
'collaborative',
|
|
@@ -130,9 +155,25 @@ function normalizeAgentBehaviorInstructions(value) {
|
|
|
130
155
|
const trimmed = value.trim();
|
|
131
156
|
return trimmed.length > 0 ? trimmed : null;
|
|
132
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* The scope-independent participation defaults. `maxConsecutiveAgentTurns` is
|
|
160
|
+
* `null` here because the turn cap is scope-dependent — a group with no stored
|
|
161
|
+
* setting resolves to {@link DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS}. Use
|
|
162
|
+
* {@link resolveAgentBehaviorPolicy} with a `conversationType` to learn what a
|
|
163
|
+
* given conversation actually enforces.
|
|
164
|
+
*/
|
|
133
165
|
function getDefaultParticipationPolicy() {
|
|
134
166
|
return { ...DEFAULT_POLICY.participation };
|
|
135
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Coalesce agent defaults and a conversation override into the resolved record
|
|
170
|
+
* every runtime reads. `conversationType` selects the Canon-wide fallbacks that
|
|
171
|
+
* differ per scope — today only the turn cap, which defaults to
|
|
172
|
+
* {@link DEFAULT_GROUP_MAX_CONSECUTIVE_AGENT_TURNS} in groups and to unlimited
|
|
173
|
+
* everywhere else. Omit it when resolving for the agent scope (no conversation);
|
|
174
|
+
* an absent or unknown type always resolves to the unlimited default, so a call
|
|
175
|
+
* site that forgets to thread it can never invent a cap.
|
|
176
|
+
*/
|
|
136
177
|
function resolveAgentBehaviorPolicy(params) {
|
|
137
178
|
const agentDefault = params?.agentDefault ?? null;
|
|
138
179
|
const conversationOverride = params?.conversationOverride ?? null;
|
|
@@ -151,7 +192,7 @@ function resolveAgentBehaviorPolicy(params) {
|
|
|
151
192
|
? conversationOverride.maxConsecutiveAgentTurns ?? null
|
|
152
193
|
: agentDefault?.maxConsecutiveAgentTurns !== undefined
|
|
153
194
|
? agentDefault.maxConsecutiveAgentTurns ?? null
|
|
154
|
-
:
|
|
195
|
+
: defaultMaxConsecutiveAgentTurns(params?.conversationType),
|
|
155
196
|
},
|
|
156
197
|
instructions: [
|
|
157
198
|
...(defaultInstructions ? [defaultInstructions] : []),
|
|
@@ -163,14 +204,22 @@ function resolveAgentBehaviorPolicy(params) {
|
|
|
163
204
|
},
|
|
164
205
|
};
|
|
165
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* The streak counts exactly the agent messages that can trigger another agent —
|
|
209
|
+
* the only messages a loop can be built from. Reusing the trigger gate keeps the
|
|
210
|
+
* two in lockstep: it admits `control` messages that do not suppress auto-reply
|
|
211
|
+
* (hand-crafted runtime metadata can otherwise drive an uncounted loop) and
|
|
212
|
+
* still drops chunk parts, interim progress, card round-trips and media/failure
|
|
213
|
+
* notices, none of which trigger anyone and none of which should burn a slot.
|
|
214
|
+
*/
|
|
166
215
|
function toHistorySenderType(message) {
|
|
167
216
|
if (message.senderType !== 'ai_agent') {
|
|
168
217
|
return 'human';
|
|
169
218
|
}
|
|
170
|
-
return (0, turnProtocol_js_1.
|
|
171
|
-
senderType:
|
|
219
|
+
return (0, turnProtocol_js_1.shouldTriggerAgentTurn)({
|
|
220
|
+
senderType: 'ai_agent',
|
|
172
221
|
metadata: message.metadata,
|
|
173
|
-
})
|
|
222
|
+
}).allow
|
|
174
223
|
? 'ai_agent'
|
|
175
224
|
: null;
|
|
176
225
|
}
|
|
@@ -209,9 +258,17 @@ function appendParticipationHistoryMessage(snapshot, message, limit = exports.PA
|
|
|
209
258
|
].slice(0, limit));
|
|
210
259
|
}
|
|
211
260
|
function evaluateParticipationPolicy(policy, input) {
|
|
212
|
-
const resolved = policy ?? resolveAgentBehaviorPolicy();
|
|
261
|
+
const resolved = policy ?? resolveAgentBehaviorPolicy({ conversationType: input.conversationType });
|
|
213
262
|
const participation = resolved.participation;
|
|
214
|
-
const
|
|
263
|
+
const priorAgentTurns = Math.max(input.consecutiveAgentTurns ?? 0, 0);
|
|
264
|
+
/** Streak length once the message being evaluated is counted. */
|
|
265
|
+
const agentTurnsWithCurrent = priorAgentTurns
|
|
266
|
+
+ (input.senderType === 'ai_agent' ? 1 : 0);
|
|
267
|
+
/**
|
|
268
|
+
* The long-running-collaboration brake reads "more than the current turn",
|
|
269
|
+
* so it keeps its historical floor rather than the exact streak length.
|
|
270
|
+
*/
|
|
271
|
+
const consecutiveAgentTurns = Math.max(priorAgentTurns, input.senderType === 'ai_agent' ? 1 : 0);
|
|
215
272
|
const currentAgentStreakStartedByHuman = input.currentAgentStreakStartedByHuman === true;
|
|
216
273
|
if (input.conversationType === 'group'
|
|
217
274
|
&& participation.requireMentionForGroupReplies
|
|
@@ -248,9 +305,13 @@ function evaluateParticipationPolicy(policy, input) {
|
|
|
248
305
|
reason: 'a fresh human steer is required before continuing agent collaboration',
|
|
249
306
|
};
|
|
250
307
|
}
|
|
308
|
+
// `maxConsecutiveAgentTurns: N` means at most N consecutive agent turns
|
|
309
|
+
// without a human in between. Delivering this message triggers turn
|
|
310
|
+
// `agentTurnsWithCurrent + 1`, so it is suppressed as soon as the streak has
|
|
311
|
+
// already reached N.
|
|
251
312
|
if (typeof participation.maxConsecutiveAgentTurns === 'number'
|
|
252
313
|
&& participation.maxConsecutiveAgentTurns >= 0
|
|
253
|
-
&&
|
|
314
|
+
&& agentTurnsWithCurrent >= participation.maxConsecutiveAgentTurns) {
|
|
254
315
|
return {
|
|
255
316
|
allow: false,
|
|
256
317
|
reasonCode: 'agent_turn_limit_reached',
|
package/dist/cjs/verbContract.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Deliberately NOT verbs: replying in the active conversation, streaming
|
|
18
18
|
* partials, typing, and read receipts stay host-mediated — the model talks
|
|
19
|
-
* and the platform delivers.
|
|
19
|
+
* and the platform delivers. `no_reply` is the deliberate exception: it is the
|
|
20
|
+
* ABSENCE of a reply, and only an intent can express that — silence has no
|
|
21
|
+
* host-mediated channel of its own. Still out of scope pending their own design
|
|
20
22
|
* pass: plan approval (the fourth runtime-interaction kind — coding-host
|
|
21
23
|
* concern today) and block/mute. Their REST surfaces remain directly
|
|
22
24
|
* callable.
|
|
@@ -42,7 +44,7 @@
|
|
|
42
44
|
* canon-verbs.limits.json) after schema validation.
|
|
43
45
|
*/
|
|
44
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.CANON_VERB_LIMITS_ARTIFACT = exports.CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = exports.VERB_NATIVE_METADATA_KEYS = exports.CANON_VERB_NAMES = exports.SELF_CONTEXT_TYPE = exports.VERB_RATE_LIMITS = exports.VERB_LIMITS = exports.VERB_ID_PATTERNS = exports.CANON_CARD_SCHEMA_ID = exports.CANON_VERB_NAMESPACE = exports.CANON_VERBS_SCHEMA_ID = exports.CANON_VERBS_SCHEMA_VERSION = void 0;
|
|
47
|
+
exports.CANON_VERB_LIMITS_ARTIFACT = exports.NO_REPLY_ACK_NOTE = exports.CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = exports.VERB_NATIVE_METADATA_KEYS = exports.CANON_VERB_NAMES = exports.SELF_CONTEXT_TYPE = exports.VERB_RATE_LIMITS = exports.VERB_LIMITS = exports.VERB_ID_PATTERNS = exports.CANON_CARD_SCHEMA_ID = exports.CANON_VERB_NAMESPACE = exports.CANON_VERBS_SCHEMA_ID = exports.CANON_VERBS_SCHEMA_VERSION = void 0;
|
|
46
48
|
exports.normalizeVerbNativeMetadata = normalizeVerbNativeMetadata;
|
|
47
49
|
exports.canonVerbToolName = canonVerbToolName;
|
|
48
50
|
exports.findVerbByteLimitViolations = findVerbByteLimitViolations;
|
|
@@ -165,6 +167,12 @@ exports.VERB_LIMITS = {
|
|
|
165
167
|
* CREATE does not length-check today — treat as the authoring contract).
|
|
166
168
|
*/
|
|
167
169
|
groupNameChars: 100,
|
|
170
|
+
/**
|
|
171
|
+
* `no_reply.reason` authoring cap. There is no server enforcement beyond the
|
|
172
|
+
* intent schema — the handler logs the reason's presence and nothing else —
|
|
173
|
+
* so this constant is the canonical figure.
|
|
174
|
+
*/
|
|
175
|
+
noReplyReasonChars: 500,
|
|
168
176
|
};
|
|
169
177
|
/**
|
|
170
178
|
* Sender-side rate limits enforced by POST /messages/send
|
|
@@ -196,6 +204,7 @@ exports.CANON_VERB_NAMES = [
|
|
|
196
204
|
'list_contacts',
|
|
197
205
|
'list_contact_requests',
|
|
198
206
|
'list_conversations',
|
|
207
|
+
'no_reply',
|
|
199
208
|
];
|
|
200
209
|
exports.VERB_NATIVE_METADATA_KEYS = [
|
|
201
210
|
'runtime',
|
|
@@ -271,6 +280,13 @@ exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = 'CREATE_GROUP_NO_ADDABLE_MEMBERS'
|
|
|
271
280
|
* creates the group from the app instead).
|
|
272
281
|
*/
|
|
273
282
|
exports.CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = 'CREATE_GROUP_CREATOR_SETUP_REQUIRED';
|
|
283
|
+
/**
|
|
284
|
+
* The `note` the server returns on a `no_reply` ack — the model's closure
|
|
285
|
+
* sentence. It lives here, not in a host, because every binding surfaces the
|
|
286
|
+
* server's result verbatim and a runtime that answers `no_reply` locally must
|
|
287
|
+
* say the same thing.
|
|
288
|
+
*/
|
|
289
|
+
exports.NO_REPLY_ACK_NOTE = 'Acknowledged — nothing was posted to the conversation.';
|
|
274
290
|
/** Flat tool name for a verb on namespaced tool surfaces (e.g. `canon_send_to`). */
|
|
275
291
|
function canonVerbToolName(verb) {
|
|
276
292
|
return `${exports.CANON_VERB_NAMESPACE}_${verb}`;
|
package/dist/cjs/verbSchemas.js
CHANGED
|
@@ -1058,6 +1058,30 @@ const list_conversations_result = {
|
|
|
1058
1058
|
},
|
|
1059
1059
|
},
|
|
1060
1060
|
};
|
|
1061
|
+
const no_reply_input = {
|
|
1062
|
+
type: 'object',
|
|
1063
|
+
description: 'End this turn without posting anything to the conversation. Nothing is '
|
|
1064
|
+
+ 'rendered and no other member or agent is triggered.',
|
|
1065
|
+
additionalProperties: false,
|
|
1066
|
+
properties: {
|
|
1067
|
+
conversationId: { type: 'string', pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId },
|
|
1068
|
+
reason: {
|
|
1069
|
+
type: 'string',
|
|
1070
|
+
maxLength: verbContract_js_1.VERB_LIMITS.noReplyReasonChars,
|
|
1071
|
+
description: 'Never rendered; logged only.',
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
const no_reply_result = {
|
|
1076
|
+
type: 'object',
|
|
1077
|
+
required: ['status'],
|
|
1078
|
+
additionalProperties: true,
|
|
1079
|
+
properties: {
|
|
1080
|
+
status: { const: 'acknowledged' },
|
|
1081
|
+
conversationId: { type: 'string' },
|
|
1082
|
+
note: { type: 'string' },
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1061
1085
|
// ---------------------------------------------------------------------------
|
|
1062
1086
|
// Bundle
|
|
1063
1087
|
// ---------------------------------------------------------------------------
|
|
@@ -1116,6 +1140,8 @@ exports.CANON_VERBS_JSON_SCHEMA = {
|
|
|
1116
1140
|
list_contact_requests_result,
|
|
1117
1141
|
list_conversations_input,
|
|
1118
1142
|
list_conversations_result,
|
|
1143
|
+
no_reply_input,
|
|
1144
|
+
no_reply_result,
|
|
1119
1145
|
},
|
|
1120
1146
|
};
|
|
1121
1147
|
exports.CANON_VERB_SCHEMA_REFS = Object.fromEntries(verbContract_js_1.CANON_VERB_NAMES.map((name) => [
|
package/dist/cjs/verbWire.js
CHANGED
|
@@ -118,6 +118,7 @@ exports.VERB_WIRE_ENVELOPE_FIELDS = {
|
|
|
118
118
|
list_contacts: { required: [], optional: [] },
|
|
119
119
|
list_contact_requests: { required: [], optional: [] },
|
|
120
120
|
list_conversations: { required: [], optional: ['limit'] },
|
|
121
|
+
no_reply: { required: [], optional: ['conversationId'] },
|
|
121
122
|
};
|
|
122
123
|
// ---------------------------------------------------------------------------
|
|
123
124
|
// JSON Schema
|
|
@@ -476,6 +477,11 @@ function projectVerbIntentToWire(verb, intent, options) {
|
|
|
476
477
|
envelope = compact({ limit: input.limit });
|
|
477
478
|
break;
|
|
478
479
|
}
|
|
480
|
+
case 'no_reply': {
|
|
481
|
+
envelope = compact({ conversationId: input.conversationId });
|
|
482
|
+
value = compact({ reason: input.reason });
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
479
485
|
}
|
|
480
486
|
return {
|
|
481
487
|
wire: exports.CANON_VERB_WIRE_SCHEMA_VERSION,
|
|
@@ -613,6 +619,8 @@ function mergeVerbWireToIntent(request) {
|
|
|
613
619
|
return {};
|
|
614
620
|
case 'list_conversations':
|
|
615
621
|
return compact({ limit: envelope.limit });
|
|
622
|
+
case 'no_reply':
|
|
623
|
+
return compact({ conversationId: envelope.conversationId, reason: value.reason });
|
|
616
624
|
}
|
|
617
625
|
}
|
|
618
626
|
/**
|
package/dist/verbContract.d.ts
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Deliberately NOT verbs: replying in the active conversation, streaming
|
|
17
17
|
* partials, typing, and read receipts stay host-mediated — the model talks
|
|
18
|
-
* and the platform delivers.
|
|
18
|
+
* and the platform delivers. `no_reply` is the deliberate exception: it is the
|
|
19
|
+
* ABSENCE of a reply, and only an intent can express that — silence has no
|
|
20
|
+
* host-mediated channel of its own. Still out of scope pending their own design
|
|
19
21
|
* pass: plan approval (the fourth runtime-interaction kind — coding-host
|
|
20
22
|
* concern today) and block/mute. Their REST surfaces remain directly
|
|
21
23
|
* callable.
|
|
@@ -159,6 +161,12 @@ export declare const VERB_LIMITS: {
|
|
|
159
161
|
* CREATE does not length-check today — treat as the authoring contract).
|
|
160
162
|
*/
|
|
161
163
|
readonly groupNameChars: 100;
|
|
164
|
+
/**
|
|
165
|
+
* `no_reply.reason` authoring cap. There is no server enforcement beyond the
|
|
166
|
+
* intent schema — the handler logs the reason's presence and nothing else —
|
|
167
|
+
* so this constant is the canonical figure.
|
|
168
|
+
*/
|
|
169
|
+
readonly noReplyReasonChars: 500;
|
|
162
170
|
};
|
|
163
171
|
/**
|
|
164
172
|
* Sender-side rate limits enforced by POST /messages/send
|
|
@@ -173,7 +181,7 @@ export declare const VERB_RATE_LIMITS: {
|
|
|
173
181
|
/** The one self-context type the platform accepts today. */
|
|
174
182
|
export declare const SELF_CONTEXT_TYPE = "cross_session";
|
|
175
183
|
/** Canonical verb names. */
|
|
176
|
-
export declare const CANON_VERB_NAMES: readonly ["send_to", "request_input", "request_approval", "check_approval", "send_card", "request_card", "share_contact", "react", "forward", "create_group", "add_member", "remove_member", "leave_conversation", "list_contacts", "list_contact_requests", "list_conversations"];
|
|
184
|
+
export declare const CANON_VERB_NAMES: readonly ["send_to", "request_input", "request_approval", "check_approval", "send_card", "request_card", "share_contact", "react", "forward", "create_group", "add_member", "remove_member", "leave_conversation", "list_contacts", "list_contact_requests", "list_conversations", "no_reply"];
|
|
177
185
|
export type CanonVerbName = (typeof CANON_VERB_NAMES)[number];
|
|
178
186
|
/** Private note-to-self attached to a cross-conversation send. */
|
|
179
187
|
export interface VerbSelfContext {
|
|
@@ -692,6 +700,25 @@ export interface VerbConversationSummary {
|
|
|
692
700
|
export interface ListConversationsResult {
|
|
693
701
|
conversations: VerbConversationSummary[];
|
|
694
702
|
}
|
|
703
|
+
/** Deliberate silence: end the turn without posting anything. */
|
|
704
|
+
export interface NoReplyInput {
|
|
705
|
+
/** Bindings default this to the active conversation when they have one. */
|
|
706
|
+
conversationId?: string;
|
|
707
|
+
/** Private rationale — logged only, never rendered. <= noReplyReasonChars. */
|
|
708
|
+
reason?: string;
|
|
709
|
+
}
|
|
710
|
+
export interface NoReplyResult {
|
|
711
|
+
status: 'acknowledged';
|
|
712
|
+
conversationId?: string;
|
|
713
|
+
note?: string;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* The `note` the server returns on a `no_reply` ack — the model's closure
|
|
717
|
+
* sentence. It lives here, not in a host, because every binding surfaces the
|
|
718
|
+
* server's result verbatim and a runtime that answers `no_reply` locally must
|
|
719
|
+
* say the same thing.
|
|
720
|
+
*/
|
|
721
|
+
export declare const NO_REPLY_ACK_NOTE = "Acknowledged \u2014 nothing was posted to the conversation.";
|
|
695
722
|
/** Flat tool name for a verb on namespaced tool surfaces (e.g. `canon_send_to`). */
|
|
696
723
|
export declare function canonVerbToolName(verb: CanonVerbName): string;
|
|
697
724
|
export interface VerbLimitViolation {
|
|
@@ -791,6 +818,12 @@ export declare const CANON_VERB_LIMITS_ARTIFACT: {
|
|
|
791
818
|
* CREATE does not length-check today — treat as the authoring contract).
|
|
792
819
|
*/
|
|
793
820
|
readonly groupNameChars: 100;
|
|
821
|
+
/**
|
|
822
|
+
* `no_reply.reason` authoring cap. There is no server enforcement beyond the
|
|
823
|
+
* intent schema — the handler logs the reason's presence and nothing else —
|
|
824
|
+
* so this constant is the canonical figure.
|
|
825
|
+
*/
|
|
826
|
+
readonly noReplyReasonChars: 500;
|
|
794
827
|
};
|
|
795
828
|
readonly rateLimits: {
|
|
796
829
|
readonly senderMessagesPer5Min: 300;
|
package/dist/verbContract.js
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Deliberately NOT verbs: replying in the active conversation, streaming
|
|
17
17
|
* partials, typing, and read receipts stay host-mediated — the model talks
|
|
18
|
-
* and the platform delivers.
|
|
18
|
+
* and the platform delivers. `no_reply` is the deliberate exception: it is the
|
|
19
|
+
* ABSENCE of a reply, and only an intent can express that — silence has no
|
|
20
|
+
* host-mediated channel of its own. Still out of scope pending their own design
|
|
19
21
|
* pass: plan approval (the fourth runtime-interaction kind — coding-host
|
|
20
22
|
* concern today) and block/mute. Their REST surfaces remain directly
|
|
21
23
|
* callable.
|
|
@@ -159,6 +161,12 @@ export const VERB_LIMITS = {
|
|
|
159
161
|
* CREATE does not length-check today — treat as the authoring contract).
|
|
160
162
|
*/
|
|
161
163
|
groupNameChars: 100,
|
|
164
|
+
/**
|
|
165
|
+
* `no_reply.reason` authoring cap. There is no server enforcement beyond the
|
|
166
|
+
* intent schema — the handler logs the reason's presence and nothing else —
|
|
167
|
+
* so this constant is the canonical figure.
|
|
168
|
+
*/
|
|
169
|
+
noReplyReasonChars: 500,
|
|
162
170
|
};
|
|
163
171
|
/**
|
|
164
172
|
* Sender-side rate limits enforced by POST /messages/send
|
|
@@ -190,6 +198,7 @@ export const CANON_VERB_NAMES = [
|
|
|
190
198
|
'list_contacts',
|
|
191
199
|
'list_contact_requests',
|
|
192
200
|
'list_conversations',
|
|
201
|
+
'no_reply',
|
|
193
202
|
];
|
|
194
203
|
export const VERB_NATIVE_METADATA_KEYS = [
|
|
195
204
|
'runtime',
|
|
@@ -265,6 +274,13 @@ export const CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = 'CREATE_GROUP_NO_ADDABLE_MEM
|
|
|
265
274
|
* creates the group from the app instead).
|
|
266
275
|
*/
|
|
267
276
|
export const CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = 'CREATE_GROUP_CREATOR_SETUP_REQUIRED';
|
|
277
|
+
/**
|
|
278
|
+
* The `note` the server returns on a `no_reply` ack — the model's closure
|
|
279
|
+
* sentence. It lives here, not in a host, because every binding surfaces the
|
|
280
|
+
* server's result verbatim and a runtime that answers `no_reply` locally must
|
|
281
|
+
* say the same thing.
|
|
282
|
+
*/
|
|
283
|
+
export const NO_REPLY_ACK_NOTE = 'Acknowledged — nothing was posted to the conversation.';
|
|
268
284
|
/** Flat tool name for a verb on namespaced tool surfaces (e.g. `canon_send_to`). */
|
|
269
285
|
export function canonVerbToolName(verb) {
|
|
270
286
|
return `${CANON_VERB_NAMESPACE}_${verb}`;
|
package/dist/verbSchemas.d.ts
CHANGED
|
@@ -1700,6 +1700,38 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
|
|
|
1700
1700
|
};
|
|
1701
1701
|
};
|
|
1702
1702
|
};
|
|
1703
|
+
readonly no_reply_input: {
|
|
1704
|
+
readonly type: "object";
|
|
1705
|
+
readonly description: string;
|
|
1706
|
+
readonly additionalProperties: false;
|
|
1707
|
+
readonly properties: {
|
|
1708
|
+
readonly conversationId: {
|
|
1709
|
+
readonly type: "string";
|
|
1710
|
+
readonly pattern: "^[A-Za-z0-9_.:-]{1,160}$";
|
|
1711
|
+
};
|
|
1712
|
+
readonly reason: {
|
|
1713
|
+
readonly type: "string";
|
|
1714
|
+
readonly maxLength: 500;
|
|
1715
|
+
readonly description: "Never rendered; logged only.";
|
|
1716
|
+
};
|
|
1717
|
+
};
|
|
1718
|
+
};
|
|
1719
|
+
readonly no_reply_result: {
|
|
1720
|
+
readonly type: "object";
|
|
1721
|
+
readonly required: readonly ["status"];
|
|
1722
|
+
readonly additionalProperties: true;
|
|
1723
|
+
readonly properties: {
|
|
1724
|
+
readonly status: {
|
|
1725
|
+
readonly const: "acknowledged";
|
|
1726
|
+
};
|
|
1727
|
+
readonly conversationId: {
|
|
1728
|
+
readonly type: "string";
|
|
1729
|
+
};
|
|
1730
|
+
readonly note: {
|
|
1731
|
+
readonly type: "string";
|
|
1732
|
+
};
|
|
1733
|
+
};
|
|
1734
|
+
};
|
|
1703
1735
|
};
|
|
1704
1736
|
};
|
|
1705
1737
|
/** Pointer pair into the bundle for one verb. */
|
package/dist/verbSchemas.js
CHANGED
|
@@ -1053,6 +1053,30 @@ const list_conversations_result = {
|
|
|
1053
1053
|
},
|
|
1054
1054
|
},
|
|
1055
1055
|
};
|
|
1056
|
+
const no_reply_input = {
|
|
1057
|
+
type: 'object',
|
|
1058
|
+
description: 'End this turn without posting anything to the conversation. Nothing is '
|
|
1059
|
+
+ 'rendered and no other member or agent is triggered.',
|
|
1060
|
+
additionalProperties: false,
|
|
1061
|
+
properties: {
|
|
1062
|
+
conversationId: { type: 'string', pattern: VERB_ID_PATTERNS.runtimeId },
|
|
1063
|
+
reason: {
|
|
1064
|
+
type: 'string',
|
|
1065
|
+
maxLength: VERB_LIMITS.noReplyReasonChars,
|
|
1066
|
+
description: 'Never rendered; logged only.',
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
};
|
|
1070
|
+
const no_reply_result = {
|
|
1071
|
+
type: 'object',
|
|
1072
|
+
required: ['status'],
|
|
1073
|
+
additionalProperties: true,
|
|
1074
|
+
properties: {
|
|
1075
|
+
status: { const: 'acknowledged' },
|
|
1076
|
+
conversationId: { type: 'string' },
|
|
1077
|
+
note: { type: 'string' },
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1056
1080
|
// ---------------------------------------------------------------------------
|
|
1057
1081
|
// Bundle
|
|
1058
1082
|
// ---------------------------------------------------------------------------
|
|
@@ -1111,6 +1135,8 @@ export const CANON_VERBS_JSON_SCHEMA = {
|
|
|
1111
1135
|
list_contact_requests_result,
|
|
1112
1136
|
list_conversations_input,
|
|
1113
1137
|
list_conversations_result,
|
|
1138
|
+
no_reply_input,
|
|
1139
|
+
no_reply_result,
|
|
1114
1140
|
},
|
|
1115
1141
|
};
|
|
1116
1142
|
export const CANON_VERB_SCHEMA_REFS = Object.fromEntries(CANON_VERB_NAMES.map((name) => [
|
package/dist/verbWire.d.ts
CHANGED
|
@@ -137,7 +137,7 @@ export declare const CANON_VERB_WIRE_JSON_SCHEMA: {
|
|
|
137
137
|
readonly const: "canon.verb-wire.v1";
|
|
138
138
|
};
|
|
139
139
|
readonly verb: {
|
|
140
|
-
readonly enum: readonly ["send_to", "request_input", "request_approval", "check_approval", "send_card", "request_card", "share_contact", "react", "forward", "create_group", "add_member", "remove_member", "leave_conversation", "list_contacts", "list_contact_requests", "list_conversations"];
|
|
140
|
+
readonly enum: readonly ["send_to", "request_input", "request_approval", "check_approval", "send_card", "request_card", "share_contact", "react", "forward", "create_group", "add_member", "remove_member", "leave_conversation", "list_contacts", "list_contact_requests", "list_conversations", "no_reply"];
|
|
141
141
|
};
|
|
142
142
|
readonly envelope: {
|
|
143
143
|
readonly $ref: "#/$defs/envelope";
|
|
@@ -509,7 +509,7 @@ export declare const CANON_VERB_WIRE_JSON_SCHEMA: {
|
|
|
509
509
|
readonly const: "canon.verb-wire.v1";
|
|
510
510
|
};
|
|
511
511
|
readonly verb: {
|
|
512
|
-
readonly enum: readonly ["send_to", "request_input", "request_approval", "check_approval", "send_card", "request_card", "share_contact", "react", "forward", "create_group", "add_member", "remove_member", "leave_conversation", "list_contacts", "list_contact_requests", "list_conversations"];
|
|
512
|
+
readonly enum: readonly ["send_to", "request_input", "request_approval", "check_approval", "send_card", "request_card", "share_contact", "react", "forward", "create_group", "add_member", "remove_member", "leave_conversation", "list_contacts", "list_contact_requests", "list_conversations", "no_reply"];
|
|
513
513
|
};
|
|
514
514
|
readonly result: {
|
|
515
515
|
readonly $ref: "#/$defs/body";
|
package/dist/verbWire.js
CHANGED
|
@@ -112,6 +112,7 @@ export const VERB_WIRE_ENVELOPE_FIELDS = {
|
|
|
112
112
|
list_contacts: { required: [], optional: [] },
|
|
113
113
|
list_contact_requests: { required: [], optional: [] },
|
|
114
114
|
list_conversations: { required: [], optional: ['limit'] },
|
|
115
|
+
no_reply: { required: [], optional: ['conversationId'] },
|
|
115
116
|
};
|
|
116
117
|
// ---------------------------------------------------------------------------
|
|
117
118
|
// JSON Schema
|
|
@@ -470,6 +471,11 @@ export function projectVerbIntentToWire(verb, intent, options) {
|
|
|
470
471
|
envelope = compact({ limit: input.limit });
|
|
471
472
|
break;
|
|
472
473
|
}
|
|
474
|
+
case 'no_reply': {
|
|
475
|
+
envelope = compact({ conversationId: input.conversationId });
|
|
476
|
+
value = compact({ reason: input.reason });
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
473
479
|
}
|
|
474
480
|
return {
|
|
475
481
|
wire: CANON_VERB_WIRE_SCHEMA_VERSION,
|
|
@@ -607,6 +613,8 @@ export function mergeVerbWireToIntent(request) {
|
|
|
607
613
|
return {};
|
|
608
614
|
case 'list_conversations':
|
|
609
615
|
return compact({ limit: envelope.limit });
|
|
616
|
+
case 'no_reply':
|
|
617
|
+
return compact({ conversationId: envelope.conversationId, reason: value.reason });
|
|
610
618
|
}
|
|
611
619
|
}
|
|
612
620
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/backend-contracts",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Canon backend contract helpers shared by Functions and stream-service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@canonmsg/rich-cards": "^0.
|
|
46
|
+
"@canonmsg/rich-cards": "^0.9.1",
|
|
47
47
|
"@types/node": "^22.0.0",
|
|
48
48
|
"ajv": "^8.20.0",
|
|
49
49
|
"typescript": "~5.7.0",
|