@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.
@@ -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 !== 'idle') {
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 !== 'idle') {
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);