@newheap/platform-ai-chat 0.13.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'];
@@ -67,6 +580,10 @@ class NhAssistantMockAdmin {
67
580
  codeDefinition: source === 'code' ? structuredClone(input) : null
68
581
  });
69
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);
70
587
  for (const { remoteTools, secret, ...input } of admin.mcpServers ?? []) {
71
588
  this.servers.set(input.id, {
72
589
  server: { ...toServerFields(input), lastSyncAt: null, lastSyncStatus: null },
@@ -141,6 +658,8 @@ class NhAssistantMockAdmin {
141
658
  return this.context(method, id, body);
142
659
  case 'tools':
143
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');
144
663
  case 'agents':
145
664
  if (id !== undefined && action === 'feedback') {
146
665
  return this.agentFeedback(method, id, child, segments.length);
@@ -151,6 +670,12 @@ class NhAssistantMockAdmin {
151
670
  if (id !== undefined && (action === 'replay' || action === 'replays') && segments.length === 4) {
152
671
  return this.agentReplay(method, id, action, body);
153
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
+ }
154
679
  if (id !== undefined && action === 'skills' && child !== undefined && segments[5] === 'state' && segments.length === 6) {
155
680
  return this.skillStateEndpoint(method, id, child, body);
156
681
  }
@@ -751,6 +1276,39 @@ class NhAssistantMockAdmin {
751
1276
  serviceAccount: previous?.serviceAccount ?? null
752
1277
  };
753
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
+ }]
1310
+ };
1311
+ }
754
1312
  toServer(stored) {
755
1313
  return {
756
1314
  ...stored.server,
@@ -931,7 +1489,7 @@ class NhAssistantMockBackend {
931
1489
  */
932
1490
  async simulateParticipantTurn(conversationId, person, text) {
933
1491
  const conversation = this.conversations.get(conversationId);
934
- if (!conversation || conversation.status !== 'idle') {
1492
+ if (!conversation || !acceptsMessage(conversation.status)) {
935
1493
  return;
936
1494
  }
937
1495
  this.addMember(conversationId, person, 'participant');
@@ -1114,7 +1672,7 @@ class NhAssistantMockBackend {
1114
1672
  if (text.length === 0 || text.length > limit || !request.clientMessageId) {
1115
1673
  return this.empty(400);
1116
1674
  }
1117
- if (conversation.status !== 'idle') {
1675
+ if (!acceptsMessage(conversation.status)) {
1118
1676
  return this.empty(409);
1119
1677
  }
1120
1678
  // Like the server: an invalid page context is dropped, never a 400.
@@ -1497,6 +2055,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImpo
1497
2055
  function delay(milliseconds) {
1498
2056
  return new Promise(resolve => setTimeout(resolve, milliseconds));
1499
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
+ }
1500
2062
  /** Numbers messages in order, as the server does, keeping numbers that are already set. */
1501
2063
  function withSequences(conversation) {
1502
2064
  let next = lastSequence(conversation);