@newheap/platform-ai-chat 0.12.0 → 0.14.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 +5 -0
- package/admin/index.d.ts +194 -5
- package/fesm2022/newheap-platform-ai-chat-admin.mjs +551 -20
- package/fesm2022/newheap-platform-ai-chat-admin.mjs.map +1 -1
- package/fesm2022/newheap-platform-ai-chat-agent-runs.mjs +2 -2
- package/fesm2022/newheap-platform-ai-chat-agent-runs.mjs.map +1 -1
- package/fesm2022/newheap-platform-ai-chat-testing.mjs +703 -6
- package/fesm2022/newheap-platform-ai-chat-testing.mjs.map +1 -1
- package/fesm2022/newheap-platform-ai-chat.mjs +631 -30
- package/fesm2022/newheap-platform-ai-chat.mjs.map +1 -1
- package/i18n/en.json +238 -4
- package/i18n/nl.json +238 -4
- package/index.d.ts +411 -6
- package/package.json +1 -1
- package/testing/index.d.ts +19 -2
|
@@ -3,6 +3,519 @@ import { InjectionToken, inject, signal, Injectable, DestroyRef, makeEnvironment
|
|
|
3
3
|
import { NH_ASSISTANT_CONFIG, applyNhAssistantEvent, normalizeNhAssistantClientContext, applyNhAssistantApprovalDecision, NH_ASSISTANT_FETCH, NhAssistantLiveService } from '@newheap/platform-ai-chat';
|
|
4
4
|
import { Subject, NEVER } from 'rxjs';
|
|
5
5
|
|
|
6
|
+
const dashCase$1 = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
7
|
+
const accountabilities = ['agent', 'subject-responsible', 'service-account'];
|
|
8
|
+
const conflicts = ['coalesce', 'queue', 'supersede', 'ignore'];
|
|
9
|
+
const maxDescription = 300;
|
|
10
|
+
const maxInstructions$1 = 20_000;
|
|
11
|
+
const maxMessage = 2_000;
|
|
12
|
+
const maxReason = 500;
|
|
13
|
+
/**
|
|
14
|
+
* In-memory run configuration as data of the mock API: run settings of agents created in the
|
|
15
|
+
* administration, skills defined as data with versions and candidates, and data triggers. Saving
|
|
16
|
+
* checks the names against the scenario's run catalog as the server checks them against the code;
|
|
17
|
+
* new skills and triggers start in shadow. The mock runs no agents, so a comparison counts nothing.
|
|
18
|
+
*/
|
|
19
|
+
class NhAssistantMockRunConfiguration {
|
|
20
|
+
constructor(scenario, agents, now) {
|
|
21
|
+
this.agents = agents;
|
|
22
|
+
this.now = now;
|
|
23
|
+
this.settings = new Map();
|
|
24
|
+
this.skills = new Map();
|
|
25
|
+
this.triggers = new Map();
|
|
26
|
+
this.catalog = {
|
|
27
|
+
triggerEvents: scenario.runCatalog?.triggerEvents ?? [],
|
|
28
|
+
subjectSets: scenario.runCatalog?.subjectSets ?? [],
|
|
29
|
+
ruleHandlers: scenario.runCatalog?.ruleHandlers ?? [],
|
|
30
|
+
approvalPolicies: scenario.runCatalog?.approvalPolicies ?? scenario.policies ?? [],
|
|
31
|
+
profiles: scenario.runCatalog?.profiles ?? []
|
|
32
|
+
};
|
|
33
|
+
this.codeTriggers = (scenario.codeTriggers ?? []).map(trigger => ({ ...trigger, skillId: trigger.skillId ?? null }));
|
|
34
|
+
}
|
|
35
|
+
/** `GET admin/run-catalog`. */
|
|
36
|
+
runCatalog(method) {
|
|
37
|
+
return method === 'GET' ? { status: 200, body: this.catalog } : { status: 405 };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `admin/agents/{id}/configuration`, `run-settings`, `skills/{skillId}[/...]` and
|
|
41
|
+
* `triggers/{triggerId}[/shadow]`; `segments` start after the agent id.
|
|
42
|
+
*/
|
|
43
|
+
handle(method, agentId, segments, body, query) {
|
|
44
|
+
const agent = this.agents.find(agentId);
|
|
45
|
+
const [area, id, action, child] = segments;
|
|
46
|
+
if (area === 'configuration' && segments.length === 1) {
|
|
47
|
+
return agent && method === 'GET' ? { status: 200, body: this.configuration(agent) } : failure$1(404, 'assistant-agent-not-found');
|
|
48
|
+
}
|
|
49
|
+
if (area === 'run-settings' && segments.length === 1) {
|
|
50
|
+
return agent ? this.runSettings(method, agent, body, query) : failure$1(404, 'assistant-agent-not-found');
|
|
51
|
+
}
|
|
52
|
+
if (area === 'skills' && id !== undefined && (segments.length === 2 || action === 'versions' || action === 'candidate')) {
|
|
53
|
+
if (!agent) {
|
|
54
|
+
return failure$1(404, 'assistant-agent-not-found');
|
|
55
|
+
}
|
|
56
|
+
if (action === 'versions' && segments.length === 3) {
|
|
57
|
+
return method === 'GET' ? { status: 200, body: this.versions(agent.id, id) } : { status: 405 };
|
|
58
|
+
}
|
|
59
|
+
if (action === 'candidate' && segments.length === 4) {
|
|
60
|
+
return this.candidate(method, agent, id, child, body);
|
|
61
|
+
}
|
|
62
|
+
return segments.length === 2 ? this.skill(method, agent, id, body, query) : null;
|
|
63
|
+
}
|
|
64
|
+
if (area === 'triggers' && id !== undefined && (segments.length === 2 || (segments.length === 3 && action === 'shadow'))) {
|
|
65
|
+
if (!agent) {
|
|
66
|
+
return failure$1(404, 'assistant-agent-not-found');
|
|
67
|
+
}
|
|
68
|
+
return action === 'shadow' ? this.triggerShadow(method, agent, id, body) : this.trigger(method, agent, id, body, query);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
configuration(agent) {
|
|
73
|
+
const dataSkills = this.liveSkills(agent.id);
|
|
74
|
+
const dataSkillIds = new Set(dataSkills.map(skill => skill.skillId));
|
|
75
|
+
return {
|
|
76
|
+
agentId: agent.id,
|
|
77
|
+
toolSelectors: [...agent.toolSelectors],
|
|
78
|
+
codeSkills: agent.source === 'code'
|
|
79
|
+
? (agent.skills ?? []).filter(skill => !dataSkillIds.has(skill.skillId)).map(skill => ({ skillId: skill.skillId, description: skill.description }))
|
|
80
|
+
: [],
|
|
81
|
+
dataSkills,
|
|
82
|
+
codeTriggers: this.codeTriggers.filter(trigger => trigger.agentId === agent.id).map(({ agentId, ...trigger }) => trigger),
|
|
83
|
+
dataTriggers: [...this.triggers.values()].filter(trigger => this.ownerOf(trigger) === agent.id).map(trigger => ({ ...trigger })),
|
|
84
|
+
problems: [],
|
|
85
|
+
source: agent.source,
|
|
86
|
+
runSettings: agent.source === 'admin' ? this.settings.get(agent.id) ?? null : null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
runSettings(method, agent, body, query) {
|
|
90
|
+
if (agent.source === 'code') {
|
|
91
|
+
return failure$1(409, 'assistant-run-settings-in-code');
|
|
92
|
+
}
|
|
93
|
+
const current = this.settings.get(agent.id);
|
|
94
|
+
if (method === 'DELETE') {
|
|
95
|
+
if (!current) {
|
|
96
|
+
return failure$1(409, 'assistant-agent-not-configured');
|
|
97
|
+
}
|
|
98
|
+
if (!versionMatches(query, current.version)) {
|
|
99
|
+
return failure$1(409, 'assistant-version-conflict');
|
|
100
|
+
}
|
|
101
|
+
if (this.liveSkills(agent.id).length > 0 || [...this.triggers.values()].some(trigger => this.ownerOf(trigger) === agent.id)) {
|
|
102
|
+
return failure$1(409, 'assistant-run-settings-in-use');
|
|
103
|
+
}
|
|
104
|
+
this.settings.delete(agent.id);
|
|
105
|
+
return { status: 204 };
|
|
106
|
+
}
|
|
107
|
+
if (method !== 'PUT') {
|
|
108
|
+
return { status: 405 };
|
|
109
|
+
}
|
|
110
|
+
const request = (body ?? {});
|
|
111
|
+
const serviceAccount = text(request.serviceAccountActorId);
|
|
112
|
+
const errors = {};
|
|
113
|
+
if (!accountabilities.includes(request.accountability)) {
|
|
114
|
+
errors['accountability'] = [request.accountability ? 'invalid' : 'required'];
|
|
115
|
+
}
|
|
116
|
+
if ((request.accountability === 'service-account') !== (serviceAccount !== null)) {
|
|
117
|
+
errors['serviceAccountActorId'] = [serviceAccount === null ? 'required' : 'invalid'];
|
|
118
|
+
}
|
|
119
|
+
if (request.allowGatewayReads && request.accountability === 'agent') {
|
|
120
|
+
errors['allowGatewayReads'] = ['invalid'];
|
|
121
|
+
}
|
|
122
|
+
if (!inRange(request.retryCount, 0, 10)) {
|
|
123
|
+
errors['retryCount'] = ['invalid'];
|
|
124
|
+
}
|
|
125
|
+
if (!inRange(request.maxConcurrentRuns, 1, 100)) {
|
|
126
|
+
errors['maxConcurrentRuns'] = ['invalid'];
|
|
127
|
+
}
|
|
128
|
+
if (!inRange(request.maxRunsPerSubjectPerDay, 1, 1_000)) {
|
|
129
|
+
errors['maxRunsPerSubjectPerDay'] = ['invalid'];
|
|
130
|
+
}
|
|
131
|
+
if (!budgetValid(request.dailyToolCalls, request.dailyTokens)) {
|
|
132
|
+
errors['dailyBudget'] = ['invalid'];
|
|
133
|
+
}
|
|
134
|
+
this.requireKnown(errors, 'approvalPolicy', request.approvalPolicy, this.catalog.approvalPolicies);
|
|
135
|
+
this.requireKnown(errors, 'triageProfile', request.triageProfile, this.catalog.profiles);
|
|
136
|
+
this.requireKnown(errors, 'ruleHandler', request.ruleHandler, this.catalog.ruleHandlers.map(handler => handler.name));
|
|
137
|
+
if (Object.keys(errors).length > 0) {
|
|
138
|
+
return failure$1(400, 'assistant-validation', errors);
|
|
139
|
+
}
|
|
140
|
+
if (serviceAccount?.startsWith('unknown')) {
|
|
141
|
+
return failure$1(400, 'assistant-service-account-not-found');
|
|
142
|
+
}
|
|
143
|
+
if ((request.expectedVersion ?? null) !== (current?.version ?? null)) {
|
|
144
|
+
return failure$1(409, 'assistant-version-conflict');
|
|
145
|
+
}
|
|
146
|
+
const saved = {
|
|
147
|
+
version: (current?.version ?? 0) + 1,
|
|
148
|
+
accountability: request.accountability,
|
|
149
|
+
serviceAccountActorId: serviceAccount,
|
|
150
|
+
delegateFromInitiator: request.delegateFromInitiator ?? false,
|
|
151
|
+
allowGatewayReads: request.allowGatewayReads ?? false,
|
|
152
|
+
elevatedReads: request.elevatedReads ?? 'restricted',
|
|
153
|
+
approvalPolicy: text(request.approvalPolicy),
|
|
154
|
+
retryCount: request.retryCount ?? 3,
|
|
155
|
+
dailyToolCalls: request.dailyToolCalls ?? null,
|
|
156
|
+
dailyTokens: request.dailyTokens ?? null,
|
|
157
|
+
maxConcurrentRuns: request.maxConcurrentRuns ?? 4,
|
|
158
|
+
maxRunsPerSubjectPerDay: request.maxRunsPerSubjectPerDay ?? 10,
|
|
159
|
+
maxTokensPerRun: request.maxTokensPerRun ?? null,
|
|
160
|
+
triageProfile: text(request.triageProfile),
|
|
161
|
+
ruleHandler: text(request.ruleHandler),
|
|
162
|
+
changedAt: this.now(),
|
|
163
|
+
changedBy: 'current-user',
|
|
164
|
+
reason: text(request.reason),
|
|
165
|
+
inEffect: true,
|
|
166
|
+
problem: null
|
|
167
|
+
};
|
|
168
|
+
this.settings.set(agent.id, saved);
|
|
169
|
+
return { status: 200, body: saved };
|
|
170
|
+
}
|
|
171
|
+
skill(method, agent, skillId, body, query) {
|
|
172
|
+
if (agent.source === 'code' && (agent.skills ?? []).some(skill => skill.skillId === skillId) && !this.skills.has(key(agent.id, skillId))) {
|
|
173
|
+
return failure$1(409, 'assistant-skill-in-code');
|
|
174
|
+
}
|
|
175
|
+
if (!this.isConfigured(agent)) {
|
|
176
|
+
return failure$1(409, 'assistant-agent-not-configured');
|
|
177
|
+
}
|
|
178
|
+
const versions = this.skills.get(key(agent.id, skillId)) ?? [];
|
|
179
|
+
const live = versions.find(version => version.status === 'live');
|
|
180
|
+
if (method === 'DELETE') {
|
|
181
|
+
if (!live) {
|
|
182
|
+
return failure$1(404, 'assistant-skill-not-found');
|
|
183
|
+
}
|
|
184
|
+
if (!versionMatches(query, live.version)) {
|
|
185
|
+
return failure$1(409, 'assistant-version-conflict');
|
|
186
|
+
}
|
|
187
|
+
if ([...this.triggers.values()].some(trigger => this.ownerOf(trigger) === agent.id && trigger.skillId === skillId)) {
|
|
188
|
+
return failure$1(409, 'assistant-skill-in-use');
|
|
189
|
+
}
|
|
190
|
+
for (const version of versions) {
|
|
191
|
+
version.status = version.status === 'candidate' ? 'discarded' : version.status === 'live' ? 'superseded' : version.status;
|
|
192
|
+
}
|
|
193
|
+
this.agents.setDataSkill(agent.id, skillId, null);
|
|
194
|
+
return { status: 204 };
|
|
195
|
+
}
|
|
196
|
+
if (method !== 'PUT') {
|
|
197
|
+
return { status: 405 };
|
|
198
|
+
}
|
|
199
|
+
const request = (body ?? {});
|
|
200
|
+
const description = text(request.description);
|
|
201
|
+
const errors = {};
|
|
202
|
+
if (!dashCase$1.test(skillId)) {
|
|
203
|
+
errors['skillId'] = ['invalid'];
|
|
204
|
+
}
|
|
205
|
+
if (description === null) {
|
|
206
|
+
errors['description'] = ['required'];
|
|
207
|
+
}
|
|
208
|
+
else if (description.length > maxDescription) {
|
|
209
|
+
errors['description'] = ['too-long'];
|
|
210
|
+
}
|
|
211
|
+
if ((request.instructions ?? '').length > maxInstructions$1) {
|
|
212
|
+
errors['instructions'] = ['too-long'];
|
|
213
|
+
}
|
|
214
|
+
if ((request.tools ?? []).some(tool => !agent.toolSelectors.some(selector => covers(selector, tool)))) {
|
|
215
|
+
errors['tools'] = ['invalid'];
|
|
216
|
+
}
|
|
217
|
+
if (!inRange(request.maxTokensPerRun, 1, Number.MAX_SAFE_INTEGER)) {
|
|
218
|
+
errors['maxTokensPerRun'] = ['invalid'];
|
|
219
|
+
}
|
|
220
|
+
if (!budgetValid(request.dailyToolCalls, request.dailyTokens)) {
|
|
221
|
+
errors['dailyBudget'] = ['invalid'];
|
|
222
|
+
}
|
|
223
|
+
if (request.apply === 'shadow' && (request.expectedVersion ?? null) === null) {
|
|
224
|
+
errors['apply'] = ['invalid'];
|
|
225
|
+
}
|
|
226
|
+
this.requireKnown(errors, 'profile', request.profile, this.catalog.profiles);
|
|
227
|
+
this.requireKnown(errors, 'ruleHandler', request.ruleHandler, this.catalog.ruleHandlers.map(handler => handler.name));
|
|
228
|
+
this.requireKnown(errors, 'approvalPolicy', request.approvalPolicy, this.catalog.approvalPolicies);
|
|
229
|
+
if (Object.keys(errors).length > 0) {
|
|
230
|
+
return failure$1(400, 'assistant-validation', errors);
|
|
231
|
+
}
|
|
232
|
+
if ((request.expectedVersion ?? null) !== (live?.version ?? null)) {
|
|
233
|
+
return failure$1(409, 'assistant-version-conflict');
|
|
234
|
+
}
|
|
235
|
+
const candidate = request.apply === 'shadow';
|
|
236
|
+
for (const version of versions) {
|
|
237
|
+
if (version.status === 'candidate') {
|
|
238
|
+
version.status = 'discarded';
|
|
239
|
+
}
|
|
240
|
+
else if (version.status === 'live' && !candidate) {
|
|
241
|
+
version.status = 'superseded';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const instructions = text(request.instructions);
|
|
245
|
+
const saved = {
|
|
246
|
+
skillId,
|
|
247
|
+
version: versions.reduce((latest, version) => Math.max(latest, version.version), 0) + 1,
|
|
248
|
+
status: candidate ? 'candidate' : 'live',
|
|
249
|
+
description: description,
|
|
250
|
+
instructions,
|
|
251
|
+
instructionsHash: instructions === null ? null : hashText$1(instructions),
|
|
252
|
+
tools: [...new Set(request.tools ?? [])],
|
|
253
|
+
profile: text(request.profile),
|
|
254
|
+
ruleHandler: text(request.ruleHandler),
|
|
255
|
+
maxTokensPerRun: request.maxTokensPerRun ?? null,
|
|
256
|
+
dailyToolCalls: request.dailyToolCalls ?? null,
|
|
257
|
+
dailyTokens: request.dailyTokens ?? null,
|
|
258
|
+
approvalPolicy: text(request.approvalPolicy),
|
|
259
|
+
changedAt: this.now(),
|
|
260
|
+
changedBy: 'current-user',
|
|
261
|
+
reason: text(request.reason),
|
|
262
|
+
inEffect: true,
|
|
263
|
+
problem: null
|
|
264
|
+
};
|
|
265
|
+
this.skills.set(key(agent.id, skillId), [...versions, saved]);
|
|
266
|
+
if (!candidate) {
|
|
267
|
+
this.agents.setDataSkill(agent.id, skillId, saved.description);
|
|
268
|
+
}
|
|
269
|
+
return { status: 200, body: toSkill(saved) };
|
|
270
|
+
}
|
|
271
|
+
candidate(method, agent, skillId, action, body) {
|
|
272
|
+
const versions = this.skills.get(key(agent.id, skillId)) ?? [];
|
|
273
|
+
const candidate = versions.find(version => version.status === 'candidate');
|
|
274
|
+
const live = versions.find(version => version.status === 'live');
|
|
275
|
+
if (action === 'comparison' && method === 'GET') {
|
|
276
|
+
if (!candidate || !live) {
|
|
277
|
+
return failure$1(404, 'assistant-skill-candidate-not-found');
|
|
278
|
+
}
|
|
279
|
+
const comparison = {
|
|
280
|
+
agentId: agent.id,
|
|
281
|
+
skillId,
|
|
282
|
+
from: candidate.changedAt,
|
|
283
|
+
to: this.now(),
|
|
284
|
+
live: emptyStats(live.version),
|
|
285
|
+
candidate: emptyStats(candidate.version),
|
|
286
|
+
pairedEvents: 0,
|
|
287
|
+
tools: []
|
|
288
|
+
};
|
|
289
|
+
return { status: 200, body: comparison };
|
|
290
|
+
}
|
|
291
|
+
if ((action !== 'promote' && action !== 'discard') || method !== 'POST') {
|
|
292
|
+
return { status: 405 };
|
|
293
|
+
}
|
|
294
|
+
const request = body;
|
|
295
|
+
if (typeof request?.version !== 'number') {
|
|
296
|
+
return failure$1(400, 'assistant-validation', { version: ['required'] });
|
|
297
|
+
}
|
|
298
|
+
if (!candidate) {
|
|
299
|
+
return failure$1(404, 'assistant-skill-candidate-not-found');
|
|
300
|
+
}
|
|
301
|
+
if (candidate.version !== request.version) {
|
|
302
|
+
return failure$1(409, 'assistant-version-conflict');
|
|
303
|
+
}
|
|
304
|
+
if (action === 'discard') {
|
|
305
|
+
candidate.status = 'discarded';
|
|
306
|
+
return { status: 204 };
|
|
307
|
+
}
|
|
308
|
+
if (live) {
|
|
309
|
+
live.status = 'superseded';
|
|
310
|
+
}
|
|
311
|
+
candidate.status = 'live';
|
|
312
|
+
this.agents.setDataSkill(agent.id, skillId, candidate.description);
|
|
313
|
+
return { status: 200, body: toSkill(candidate) };
|
|
314
|
+
}
|
|
315
|
+
versions(agentId, skillId) {
|
|
316
|
+
return [...(this.skills.get(key(agentId, skillId)) ?? [])]
|
|
317
|
+
.sort((left, right) => right.version - left.version)
|
|
318
|
+
.map(version => ({
|
|
319
|
+
version: version.version,
|
|
320
|
+
status: version.status,
|
|
321
|
+
description: version.description,
|
|
322
|
+
instructionsHash: version.instructionsHash,
|
|
323
|
+
createdAt: version.changedAt,
|
|
324
|
+
createdBy: version.changedBy,
|
|
325
|
+
reason: version.reason
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
trigger(method, agent, triggerId, body, query) {
|
|
329
|
+
if (this.codeTriggers.some(trigger => trigger.triggerId === triggerId)) {
|
|
330
|
+
return failure$1(409, 'assistant-trigger-in-code');
|
|
331
|
+
}
|
|
332
|
+
if (!this.isConfigured(agent)) {
|
|
333
|
+
return failure$1(409, 'assistant-agent-not-configured');
|
|
334
|
+
}
|
|
335
|
+
const existing = this.triggers.get(triggerId);
|
|
336
|
+
if (existing && this.ownerOf(existing) !== agent.id) {
|
|
337
|
+
return failure$1(400, 'assistant-validation', { triggerId: ['invalid'] });
|
|
338
|
+
}
|
|
339
|
+
if (method === 'DELETE') {
|
|
340
|
+
if (!existing) {
|
|
341
|
+
return failure$1(404, 'assistant-trigger-not-found');
|
|
342
|
+
}
|
|
343
|
+
if (!versionMatches(query, existing.version)) {
|
|
344
|
+
return failure$1(409, 'assistant-version-conflict');
|
|
345
|
+
}
|
|
346
|
+
this.triggers.delete(triggerId);
|
|
347
|
+
return { status: 204 };
|
|
348
|
+
}
|
|
349
|
+
if (method !== 'PUT') {
|
|
350
|
+
return { status: 405 };
|
|
351
|
+
}
|
|
352
|
+
const request = (body ?? {});
|
|
353
|
+
const schedule = request.source === 'schedule';
|
|
354
|
+
const errors = {};
|
|
355
|
+
if (!dashCase$1.test(triggerId)) {
|
|
356
|
+
errors['triggerId'] = ['invalid'];
|
|
357
|
+
}
|
|
358
|
+
if (request.source !== 'event' && request.source !== 'schedule') {
|
|
359
|
+
errors['source'] = [request.source ? 'invalid' : 'required'];
|
|
360
|
+
}
|
|
361
|
+
if (request.conflict && !conflicts.includes(request.conflict)) {
|
|
362
|
+
errors['conflict'] = ['invalid'];
|
|
363
|
+
}
|
|
364
|
+
if (!inRange(request.debounceSeconds, 0, 3_600)) {
|
|
365
|
+
errors['debounceSeconds'] = ['invalid'];
|
|
366
|
+
}
|
|
367
|
+
if (request.source === 'event') {
|
|
368
|
+
this.requireKnown(errors, 'event', request.event, this.catalog.triggerEvents.map(item => item.name), true);
|
|
369
|
+
}
|
|
370
|
+
if (schedule) {
|
|
371
|
+
if ((text(request.cron) ?? '').split(' ').filter(field => field.length > 0).length < 5) {
|
|
372
|
+
errors['cron'] = [text(request.cron) === null ? 'required' : 'invalid'];
|
|
373
|
+
}
|
|
374
|
+
this.requireKnown(errors, 'subjectSet', request.subjectSet, this.catalog.subjectSets.map(item => item.name), true);
|
|
375
|
+
const message = text(request.message);
|
|
376
|
+
if (message === null) {
|
|
377
|
+
errors['message'] = ['required'];
|
|
378
|
+
}
|
|
379
|
+
else if (message.length > maxMessage) {
|
|
380
|
+
errors['message'] = ['too-long'];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const skill = text(request.skill);
|
|
384
|
+
if (skill !== null && !this.skillIds(agent).includes(skill)) {
|
|
385
|
+
errors['skill'] = ['not-found'];
|
|
386
|
+
}
|
|
387
|
+
if (Object.keys(errors).length > 0) {
|
|
388
|
+
return failure$1(400, 'assistant-validation', errors);
|
|
389
|
+
}
|
|
390
|
+
if ((request.expectedVersion ?? null) !== (existing?.version ?? null)) {
|
|
391
|
+
return failure$1(409, 'assistant-version-conflict');
|
|
392
|
+
}
|
|
393
|
+
const saved = {
|
|
394
|
+
triggerId,
|
|
395
|
+
skillId: skill,
|
|
396
|
+
source: request.source,
|
|
397
|
+
event: schedule ? null : text(request.event),
|
|
398
|
+
cron: schedule ? text(request.cron) : null,
|
|
399
|
+
timeZone: schedule ? text(request.timeZone) : null,
|
|
400
|
+
subjectSet: schedule ? text(request.subjectSet) : null,
|
|
401
|
+
message: schedule ? text(request.message) : null,
|
|
402
|
+
conflict: request.conflict ?? (schedule ? 'ignore' : 'coalesce'),
|
|
403
|
+
debounceSeconds: schedule ? 0 : request.debounceSeconds ?? 120,
|
|
404
|
+
shadow: existing?.shadow ?? true,
|
|
405
|
+
version: (existing?.version ?? 0) + 1,
|
|
406
|
+
changedAt: this.now(),
|
|
407
|
+
changedBy: 'current-user',
|
|
408
|
+
reason: text(request.reason),
|
|
409
|
+
inEffect: true,
|
|
410
|
+
problem: null
|
|
411
|
+
};
|
|
412
|
+
this.triggers.set(triggerId, Object.assign(saved, { [ownerKey]: agent.id }));
|
|
413
|
+
return { status: 200, body: withoutOwner(saved) };
|
|
414
|
+
}
|
|
415
|
+
triggerShadow(method, agent, triggerId, body) {
|
|
416
|
+
if (method !== 'POST') {
|
|
417
|
+
return { status: 405 };
|
|
418
|
+
}
|
|
419
|
+
const existing = this.triggers.get(triggerId);
|
|
420
|
+
if (!existing || this.ownerOf(existing) !== agent.id) {
|
|
421
|
+
return failure$1(404, 'assistant-trigger-not-found');
|
|
422
|
+
}
|
|
423
|
+
const request = body;
|
|
424
|
+
if (request?.mode !== 'shadow' && request?.mode !== 'live') {
|
|
425
|
+
return failure$1(400, 'assistant-validation', { mode: [request?.mode === undefined ? 'required' : 'invalid'] });
|
|
426
|
+
}
|
|
427
|
+
if (typeof request.reason === 'string' && request.reason.length > maxReason) {
|
|
428
|
+
return failure$1(400, 'assistant-validation', { reason: ['too-long'] });
|
|
429
|
+
}
|
|
430
|
+
existing.shadow = request.mode === 'shadow';
|
|
431
|
+
existing.changedAt = this.now();
|
|
432
|
+
existing.changedBy = 'current-user';
|
|
433
|
+
existing.reason = typeof request.reason === 'string' && request.reason.trim().length > 0 ? request.reason.trim() : null;
|
|
434
|
+
return { status: 200, body: withoutOwner(existing) };
|
|
435
|
+
}
|
|
436
|
+
/** An agent in code has a run configuration; one created in the administration once it has run settings. */
|
|
437
|
+
isConfigured(agent) {
|
|
438
|
+
return agent.source === 'code' || this.settings.has(agent.id);
|
|
439
|
+
}
|
|
440
|
+
skillIds(agent) {
|
|
441
|
+
return [...(agent.skills ?? []).map(skill => skill.skillId), ...this.liveSkills(agent.id).map(skill => skill.skillId)];
|
|
442
|
+
}
|
|
443
|
+
liveSkills(agentId) {
|
|
444
|
+
const result = [];
|
|
445
|
+
for (const [stored, versions] of this.skills) {
|
|
446
|
+
if (!stored.startsWith(`${agentId}/`)) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const live = versions.find(version => version.status === 'live');
|
|
450
|
+
if (live) {
|
|
451
|
+
const candidate = versions.find(version => version.status === 'candidate');
|
|
452
|
+
result.push({ ...toSkill(live), candidate: candidate ? toSkill(candidate) : null });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return result.sort((left, right) => left.skillId.localeCompare(right.skillId));
|
|
456
|
+
}
|
|
457
|
+
ownerOf(trigger) {
|
|
458
|
+
return trigger[ownerKey];
|
|
459
|
+
}
|
|
460
|
+
requireKnown(errors, field, value, known, required = false) {
|
|
461
|
+
const name = text(value);
|
|
462
|
+
if (name === null) {
|
|
463
|
+
if (required) {
|
|
464
|
+
errors[field] = ['required'];
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (!known.includes(name)) {
|
|
469
|
+
errors[field] = ['not-found'];
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const ownerKey = '__agentId';
|
|
474
|
+
function key(agentId, skillId) {
|
|
475
|
+
return `${agentId}/${skillId}`;
|
|
476
|
+
}
|
|
477
|
+
function toSkill({ status, ...skill }) {
|
|
478
|
+
return { ...skill, tools: [...skill.tools] };
|
|
479
|
+
}
|
|
480
|
+
function withoutOwner(trigger) {
|
|
481
|
+
const { [ownerKey]: owner, ...rest } = trigger;
|
|
482
|
+
return { ...rest };
|
|
483
|
+
}
|
|
484
|
+
function emptyStats(version) {
|
|
485
|
+
return { version, runs: 0, completed: 0, failed: 0, proposals: 0, toolCalls: 0, tokens: 0 };
|
|
486
|
+
}
|
|
487
|
+
function text(value) {
|
|
488
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
489
|
+
}
|
|
490
|
+
function inRange(value, min, max) {
|
|
491
|
+
return value === null || value === undefined || (Number.isInteger(value) && value >= min && value <= max);
|
|
492
|
+
}
|
|
493
|
+
function budgetValid(calls, tokens) {
|
|
494
|
+
const hasCalls = calls !== null && calls !== undefined;
|
|
495
|
+
const hasTokens = tokens !== null && tokens !== undefined;
|
|
496
|
+
return hasCalls === hasTokens && (!hasCalls || (calls >= 1 && tokens >= 1));
|
|
497
|
+
}
|
|
498
|
+
function versionMatches(query, version) {
|
|
499
|
+
const expected = query.get('expectedVersion');
|
|
500
|
+
return expected === null || Number(expected) === version;
|
|
501
|
+
}
|
|
502
|
+
/** The agent's selector covers the skill's: the same tool, or a prefix selector that includes it. */
|
|
503
|
+
function covers(agentSelector, skillSelector) {
|
|
504
|
+
return agentSelector === skillSelector
|
|
505
|
+
|| (agentSelector.endsWith('.*') && skillSelector.startsWith(agentSelector.slice(0, -1)));
|
|
506
|
+
}
|
|
507
|
+
function hashText$1(text) {
|
|
508
|
+
let hash = 0x811c9dc5;
|
|
509
|
+
for (let index = 0; index < text.length; index++) {
|
|
510
|
+
hash ^= text.charCodeAt(index);
|
|
511
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
512
|
+
}
|
|
513
|
+
return `fnv1a:${hash.toString(16).padStart(8, '0')}`;
|
|
514
|
+
}
|
|
515
|
+
function failure$1(status, code, errors) {
|
|
516
|
+
return { status, body: { code, messageKey: `nh-assistant.errors.${code}`, ...(errors ? { errors } : {}) } };
|
|
517
|
+
}
|
|
518
|
+
|
|
6
519
|
const dashCase = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
520
|
const styles = ['default', 'direct', 'personal', 'detailed'];
|
|
8
521
|
const addressForms = ['informal', 'formal'];
|
|
@@ -37,11 +550,16 @@ class NhAssistantMockAdmin {
|
|
|
37
550
|
this.contextVersions.push({ version: 1, hash: hashText(admin.context ?? ''), updatedAt: now(), updatedBy: null, text: admin.context ?? '' });
|
|
38
551
|
const agents = admin.agents
|
|
39
552
|
?? scenario.agents.map(agent => ({ ...codeAgentFromSummary(agent), source: 'code' }));
|
|
40
|
-
for (const { source, skills, shadowOf, ...input } of agents) {
|
|
553
|
+
for (const { source, skills, shadowOf, shadow, serviceAccount, ...input } of agents) {
|
|
554
|
+
const agentShadow = shadowOf !== undefined || shadow === true;
|
|
41
555
|
this.agents.set(input.id, {
|
|
42
556
|
agent: {
|
|
43
557
|
...this.toAgent(input, source, 1, false),
|
|
44
558
|
shadowOf: shadowOf ?? null,
|
|
559
|
+
shadow: shadowState(agentShadow, shadowOf !== undefined || shadow === true, null, shadowOf !== undefined),
|
|
560
|
+
serviceAccount: serviceAccount === undefined
|
|
561
|
+
? null
|
|
562
|
+
: { actorId: serviceAccount, configuredActorId: serviceAccount, overridden: false, changedAt: null, changedBy: null, reason: null },
|
|
45
563
|
skills: (skills ?? []).map((skill) => ({
|
|
46
564
|
skillId: skill.skillId,
|
|
47
565
|
description: skill.description,
|
|
@@ -55,12 +573,17 @@ class NhAssistantMockAdmin {
|
|
|
55
573
|
dailyToolCalls: null,
|
|
56
574
|
dailyTokens: null,
|
|
57
575
|
cancelledRuns: 0,
|
|
58
|
-
startedRuns: 0
|
|
576
|
+
startedRuns: 0,
|
|
577
|
+
shadow: shadowState(agentShadow || skill.shadow === true, skill.shadow === true, null, shadowOf !== undefined)
|
|
59
578
|
}))
|
|
60
579
|
},
|
|
61
580
|
codeDefinition: source === 'code' ? structuredClone(input) : null
|
|
62
581
|
});
|
|
63
582
|
}
|
|
583
|
+
this.runConfiguration = new NhAssistantMockRunConfiguration(admin, {
|
|
584
|
+
find: agentId => this.agents.get(agentId)?.agent,
|
|
585
|
+
setDataSkill: (agentId, skillId, description) => this.setDataSkill(agentId, skillId, description)
|
|
586
|
+
}, now);
|
|
64
587
|
for (const { remoteTools, secret, ...input } of admin.mcpServers ?? []) {
|
|
65
588
|
this.servers.set(input.id, {
|
|
66
589
|
server: { ...toServerFields(input), lastSyncAt: null, lastSyncStatus: null },
|
|
@@ -135,6 +658,8 @@ class NhAssistantMockAdmin {
|
|
|
135
658
|
return this.context(method, id, body);
|
|
136
659
|
case 'tools':
|
|
137
660
|
return method === 'GET' && segments.length === 2 ? { status: 200, body: this.toolCatalog() } : failure(404, 'assistant-not-found');
|
|
661
|
+
case 'run-catalog':
|
|
662
|
+
return segments.length === 2 ? this.runConfiguration.runCatalog(method) : failure(404, 'assistant-not-found');
|
|
138
663
|
case 'agents':
|
|
139
664
|
if (id !== undefined && action === 'feedback') {
|
|
140
665
|
return this.agentFeedback(method, id, child, segments.length);
|
|
@@ -145,9 +670,18 @@ class NhAssistantMockAdmin {
|
|
|
145
670
|
if (id !== undefined && (action === 'replay' || action === 'replays') && segments.length === 4) {
|
|
146
671
|
return this.agentReplay(method, id, action, body);
|
|
147
672
|
}
|
|
673
|
+
if (id !== undefined && action !== undefined) {
|
|
674
|
+
const configured = this.runConfiguration.handle(method, id, segments.slice(3), body, query);
|
|
675
|
+
if (configured) {
|
|
676
|
+
return configured;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
148
679
|
if (id !== undefined && action === 'skills' && child !== undefined && segments[5] === 'state' && segments.length === 6) {
|
|
149
680
|
return this.skillStateEndpoint(method, id, child, body);
|
|
150
681
|
}
|
|
682
|
+
if (id !== undefined && action === 'skills' && child !== undefined && segments[5] === 'shadow' && segments.length === 6) {
|
|
683
|
+
return this.skillShadowEndpoint(method, id, child, body);
|
|
684
|
+
}
|
|
151
685
|
return this.agentsEndpoint(method, id, action, body, segments.length);
|
|
152
686
|
case 'mcp-servers':
|
|
153
687
|
return this.serversEndpoint(method, id, action, child, body, segments.length);
|
|
@@ -228,6 +762,12 @@ class NhAssistantMockAdmin {
|
|
|
228
762
|
if (length === 4 && action === 'state') {
|
|
229
763
|
return this.agentStateEndpoint(stored, method, body);
|
|
230
764
|
}
|
|
765
|
+
if (length === 4 && action === 'shadow' && method === 'POST') {
|
|
766
|
+
return this.agentShadowEndpoint(stored, body);
|
|
767
|
+
}
|
|
768
|
+
if (length === 4 && action === 'service-account' && method === 'POST') {
|
|
769
|
+
return this.serviceAccountEndpoint(stored, body);
|
|
770
|
+
}
|
|
231
771
|
if (length === 4 && action === 'kill-switch' && method === 'POST') {
|
|
232
772
|
const engaged = body?.engaged;
|
|
233
773
|
if (typeof engaged !== 'boolean') {
|
|
@@ -286,6 +826,106 @@ class NhAssistantMockAdmin {
|
|
|
286
826
|
stored.agent = { ...stored.agent, skills: stored.agent.skills.map(item => item.skillId === skillId ? changed : item) };
|
|
287
827
|
return { status: 200, body: changed };
|
|
288
828
|
}
|
|
829
|
+
/**
|
|
830
|
+
* `POST admin/agents/{id}/shadow`: the agent's shadow mode as an override of the code. A
|
|
831
|
+
* variant always works in shadow; the skills' effective modes follow the agent's.
|
|
832
|
+
*/
|
|
833
|
+
agentShadowEndpoint(stored, body) {
|
|
834
|
+
const request = shadowRequest(body);
|
|
835
|
+
if (request === null) {
|
|
836
|
+
return validationFailure();
|
|
837
|
+
}
|
|
838
|
+
const current = stored.agent.shadow ?? shadowState(false, false, null, false);
|
|
839
|
+
if (current.locked) {
|
|
840
|
+
return failure(409, 'assistant-shadow-variant');
|
|
841
|
+
}
|
|
842
|
+
const override = request.mode === 'code' ? null : request.mode === 'shadow';
|
|
843
|
+
if (current.override !== override) {
|
|
844
|
+
const shadow = override ?? current.configured;
|
|
845
|
+
stored.agent = {
|
|
846
|
+
...stored.agent,
|
|
847
|
+
shadow: { ...current, shadow, override, changedAt: this.now(), changedBy: 'current-user', reason: request.reason },
|
|
848
|
+
skills: (stored.agent.skills ?? []).map(skill => ({ ...skill, shadow: withAgentShadow(skill.shadow, shadow) }))
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
return { status: 200, body: toAgentState(stored.agent) };
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* `POST admin/agents/{id}/skills/{skillId}/shadow`: one skill's shadow mode; a skill of an agent
|
|
855
|
+
* in shadow works in shadow whatever its own mode.
|
|
856
|
+
*/
|
|
857
|
+
skillShadowEndpoint(method, agentId, skillId, body) {
|
|
858
|
+
const stored = this.agents.get(agentId);
|
|
859
|
+
const skill = stored?.agent.skills?.find(item => item.skillId === skillId);
|
|
860
|
+
if (!stored || !skill) {
|
|
861
|
+
return failure(404, 'assistant-skill-not-found');
|
|
862
|
+
}
|
|
863
|
+
if (method !== 'POST') {
|
|
864
|
+
return { status: 405 };
|
|
865
|
+
}
|
|
866
|
+
const request = shadowRequest(body);
|
|
867
|
+
if (request === null) {
|
|
868
|
+
return validationFailure();
|
|
869
|
+
}
|
|
870
|
+
const agentShadow = stored.agent.shadow ?? shadowState(false, false, null, false);
|
|
871
|
+
if (agentShadow.locked) {
|
|
872
|
+
return failure(409, 'assistant-shadow-variant');
|
|
873
|
+
}
|
|
874
|
+
const current = skill.shadow ?? shadowState(agentShadow.shadow, false, null, false);
|
|
875
|
+
const override = request.mode === 'code' ? null : request.mode === 'shadow';
|
|
876
|
+
const changed = current.override === override
|
|
877
|
+
? skill
|
|
878
|
+
: {
|
|
879
|
+
...skill,
|
|
880
|
+
shadow: {
|
|
881
|
+
...current,
|
|
882
|
+
shadow: agentShadow.shadow || (override ?? current.configured),
|
|
883
|
+
override,
|
|
884
|
+
changedAt: this.now(),
|
|
885
|
+
changedBy: 'current-user',
|
|
886
|
+
reason: request.reason
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
stored.agent = { ...stored.agent, skills: stored.agent.skills.map(item => item.skillId === skillId ? changed : item) };
|
|
890
|
+
return { status: 200, body: changed };
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* `POST admin/agents/{id}/service-account`: the technical account of an agent that answers to
|
|
894
|
+
* one in code. The mock accepts every account id except those starting with `unknown`.
|
|
895
|
+
*/
|
|
896
|
+
serviceAccountEndpoint(stored, body) {
|
|
897
|
+
const current = stored.agent.serviceAccount;
|
|
898
|
+
if (!current) {
|
|
899
|
+
return failure(409, 'assistant-service-account-not-used');
|
|
900
|
+
}
|
|
901
|
+
const request = body;
|
|
902
|
+
if (request === undefined || (request.actorId !== null && request.actorId !== undefined && typeof request.actorId !== 'string')) {
|
|
903
|
+
return validationFailure();
|
|
904
|
+
}
|
|
905
|
+
const actorId = typeof request.actorId === 'string' && request.actorId.trim().length > 0 ? request.actorId.trim() : null;
|
|
906
|
+
if (actorId !== null && (actorId.length > 256 || actorId.startsWith('unknown'))) {
|
|
907
|
+
return actorId.length > 256 ? validationFailure() : failure(400, 'assistant-service-account-not-found');
|
|
908
|
+
}
|
|
909
|
+
const reason = typeof request.reason === 'string' && request.reason.trim().length > 0 ? request.reason.trim() : null;
|
|
910
|
+
if (reason !== null && reason.length > maxStateReason) {
|
|
911
|
+
return validationFailure();
|
|
912
|
+
}
|
|
913
|
+
const overridden = actorId !== null;
|
|
914
|
+
if ((current.overridden ? current.actorId : null) !== actorId) {
|
|
915
|
+
stored.agent = {
|
|
916
|
+
...stored.agent,
|
|
917
|
+
serviceAccount: {
|
|
918
|
+
...current,
|
|
919
|
+
actorId: actorId ?? current.configuredActorId,
|
|
920
|
+
overridden,
|
|
921
|
+
changedAt: this.now(),
|
|
922
|
+
changedBy: 'current-user',
|
|
923
|
+
reason
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return { status: 200, body: toAgentState(stored.agent) };
|
|
928
|
+
}
|
|
289
929
|
/**
|
|
290
930
|
* `GET` and `POST admin/agents/{id}/state`. The mock runs no agents on their own, so a pause
|
|
291
931
|
* collects no waiting events and a stop cancels no runs; the version never changes.
|
|
@@ -631,7 +1271,42 @@ class NhAssistantMockAdmin {
|
|
|
631
1271
|
stateReason: previous?.stateReason ?? null,
|
|
632
1272
|
waitingInputs: previous?.waitingInputs ?? 0,
|
|
633
1273
|
skills: previous?.skills ?? [],
|
|
634
|
-
shadowOf: previous?.shadowOf ?? null
|
|
1274
|
+
shadowOf: previous?.shadowOf ?? null,
|
|
1275
|
+
shadow: previous?.shadow ?? shadowState(false, false, null, false),
|
|
1276
|
+
serviceAccount: previous?.serviceAccount ?? null
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* A skill defined as data shows among the agent's skills as a skill in shadow by default, as the
|
|
1281
|
+
* server shows it; removing it takes its state along.
|
|
1282
|
+
*/
|
|
1283
|
+
setDataSkill(agentId, skillId, description) {
|
|
1284
|
+
const stored = this.agents.get(agentId);
|
|
1285
|
+
if (!stored) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const others = (stored.agent.skills ?? []).filter(skill => skill.skillId !== skillId);
|
|
1289
|
+
const existing = (stored.agent.skills ?? []).find(skill => skill.skillId === skillId);
|
|
1290
|
+
stored.agent = {
|
|
1291
|
+
...stored.agent,
|
|
1292
|
+
skills: description === null
|
|
1293
|
+
? others
|
|
1294
|
+
: [...others, existing ? { ...existing, description } : {
|
|
1295
|
+
skillId,
|
|
1296
|
+
description,
|
|
1297
|
+
runState: 'active',
|
|
1298
|
+
changedAt: null,
|
|
1299
|
+
changedBy: null,
|
|
1300
|
+
reason: null,
|
|
1301
|
+
waitingInputs: 0,
|
|
1302
|
+
toolCallsToday: 0,
|
|
1303
|
+
tokensToday: 0,
|
|
1304
|
+
dailyToolCalls: null,
|
|
1305
|
+
dailyTokens: null,
|
|
1306
|
+
cancelledRuns: 0,
|
|
1307
|
+
startedRuns: 0,
|
|
1308
|
+
shadow: shadowState(true, true, null, stored.agent.shadow?.locked ?? false)
|
|
1309
|
+
}]
|
|
635
1310
|
};
|
|
636
1311
|
}
|
|
637
1312
|
toServer(stored) {
|
|
@@ -668,9 +1343,27 @@ function toAgentState(agent) {
|
|
|
668
1343
|
cancelledRuns: 0,
|
|
669
1344
|
startedRuns: 0,
|
|
670
1345
|
skills: agent.skills ?? [],
|
|
671
|
-
shadowOf: agent.shadowOf ?? null
|
|
1346
|
+
shadowOf: agent.shadowOf ?? null,
|
|
1347
|
+
shadow: agent.shadow ?? shadowState(false, false, null, false),
|
|
1348
|
+
serviceAccount: agent.serviceAccount ?? null
|
|
672
1349
|
};
|
|
673
1350
|
}
|
|
1351
|
+
function shadowState(shadow, configured, override, locked) {
|
|
1352
|
+
return { shadow, configured, override, locked, changedAt: null, changedBy: null, reason: null };
|
|
1353
|
+
}
|
|
1354
|
+
/** A skill's mode after its agent's mode changed: the agent's shadow comes on top of its own. */
|
|
1355
|
+
function withAgentShadow(skill, agentShadow) {
|
|
1356
|
+
const own = skill?.override ?? skill?.configured ?? false;
|
|
1357
|
+
return { ...(skill ?? shadowState(false, false, null, false)), shadow: agentShadow || own };
|
|
1358
|
+
}
|
|
1359
|
+
function shadowRequest(body) {
|
|
1360
|
+
const request = body;
|
|
1361
|
+
if (request?.mode !== 'shadow' && request?.mode !== 'live' && request?.mode !== 'code') {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
const reason = typeof request.reason === 'string' && request.reason.trim().length > 0 ? request.reason.trim() : null;
|
|
1365
|
+
return reason !== null && reason.length > maxStateReason ? null : { mode: request.mode, reason };
|
|
1366
|
+
}
|
|
674
1367
|
function toServerFields(input) {
|
|
675
1368
|
return {
|
|
676
1369
|
id: input.id,
|
|
@@ -796,7 +1489,7 @@ class NhAssistantMockBackend {
|
|
|
796
1489
|
*/
|
|
797
1490
|
async simulateParticipantTurn(conversationId, person, text) {
|
|
798
1491
|
const conversation = this.conversations.get(conversationId);
|
|
799
|
-
if (!conversation || conversation.status
|
|
1492
|
+
if (!conversation || !acceptsMessage(conversation.status)) {
|
|
800
1493
|
return;
|
|
801
1494
|
}
|
|
802
1495
|
this.addMember(conversationId, person, 'participant');
|
|
@@ -979,7 +1672,7 @@ class NhAssistantMockBackend {
|
|
|
979
1672
|
if (text.length === 0 || text.length > limit || !request.clientMessageId) {
|
|
980
1673
|
return this.empty(400);
|
|
981
1674
|
}
|
|
982
|
-
if (conversation.status
|
|
1675
|
+
if (!acceptsMessage(conversation.status)) {
|
|
983
1676
|
return this.empty(409);
|
|
984
1677
|
}
|
|
985
1678
|
// Like the server: an invalid page context is dropped, never a 400.
|
|
@@ -1362,6 +2055,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImpo
|
|
|
1362
2055
|
function delay(milliseconds) {
|
|
1363
2056
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
1364
2057
|
}
|
|
2058
|
+
/** Like the server: a new message starts in an idle conversation or after a failed turn. */
|
|
2059
|
+
function acceptsMessage(status) {
|
|
2060
|
+
return status === 'idle' || status === 'failed';
|
|
2061
|
+
}
|
|
1365
2062
|
/** Numbers messages in order, as the server does, keeping numbers that are already set. */
|
|
1366
2063
|
function withSequences(conversation) {
|
|
1367
2064
|
let next = lastSequence(conversation);
|