@newheap/platform-ai-chat 0.10.0 → 0.11.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.
@@ -10,6 +10,7 @@ const responseLengths = ['short', 'normal', 'long'];
10
10
  const autonomyLevels = ['observe', 'explain', 'propose', 'simulate', 'execute'];
11
11
  const authModes = ['none', 'bearer', 'api-key', 'forward-user-token'];
12
12
  const maxInstructions = 20_000;
13
+ const maxStateReason = 500;
13
14
  const maxCustomInstructions = 1_000;
14
15
  const defaultPreferences = {
15
16
  style: 'default',
@@ -28,15 +29,35 @@ class NhAssistantMockAdmin {
28
29
  this.now = now;
29
30
  this.agents = new Map();
30
31
  this.servers = new Map();
32
+ this.replays = new Map();
31
33
  this.contextVersions = [];
32
34
  const admin = scenario.admin ?? {};
33
35
  this.canAdminister = admin.canAdminister ?? true;
34
36
  this.preferences = { ...defaultPreferences, ...scenario.preferences };
35
37
  this.contextVersions.push({ version: 1, hash: hashText(admin.context ?? ''), updatedAt: now(), updatedBy: null, text: admin.context ?? '' });
36
- const agents = admin.agents ?? scenario.agents.map(agent => ({ ...codeAgentFromSummary(agent), source: 'code' }));
37
- for (const { source, ...input } of agents) {
38
+ const agents = admin.agents
39
+ ?? scenario.agents.map(agent => ({ ...codeAgentFromSummary(agent), source: 'code' }));
40
+ for (const { source, skills, shadowOf, ...input } of agents) {
38
41
  this.agents.set(input.id, {
39
- agent: this.toAgent(input, source, 1, false),
42
+ agent: {
43
+ ...this.toAgent(input, source, 1, false),
44
+ shadowOf: shadowOf ?? null,
45
+ skills: (skills ?? []).map((skill) => ({
46
+ skillId: skill.skillId,
47
+ description: skill.description,
48
+ runState: 'active',
49
+ changedAt: null,
50
+ changedBy: null,
51
+ reason: null,
52
+ waitingInputs: 0,
53
+ toolCallsToday: 0,
54
+ tokensToday: 0,
55
+ dailyToolCalls: null,
56
+ dailyTokens: null,
57
+ cancelledRuns: 0,
58
+ startedRuns: 0
59
+ }))
60
+ },
40
61
  codeDefinition: source === 'code' ? structuredClone(input) : null
41
62
  });
42
63
  }
@@ -62,11 +83,11 @@ class NhAssistantMockAdmin {
62
83
  stored.remoteTools = structuredClone(tools);
63
84
  }
64
85
  }
65
- /** Chat agents: enabled code agents keep their keys, admin agents carry literal text. */
86
+ /** Chat agents: code agents keep their keys, admin agents carry literal text; stopped agents are left out. */
66
87
  chatAgents() {
67
88
  const result = [];
68
89
  for (const { agent } of this.agents.values()) {
69
- if (!agent.isEnabled) {
90
+ if (agent.runState === 'stopped' || agent.runState === 'configured-off') {
70
91
  continue;
71
92
  }
72
93
  const summary = this.scenario.agents.find(item => item.id === agent.id);
@@ -104,7 +125,7 @@ class NhAssistantMockAdmin {
104
125
  };
105
126
  return { status: 200, body: this.preferences };
106
127
  }
107
- handleAdmin(method, segments, body) {
128
+ handleAdmin(method, segments, body, query = new URLSearchParams()) {
108
129
  if (!this.canAdminister) {
109
130
  return failure(403, 'assistant-forbidden');
110
131
  }
@@ -118,6 +139,15 @@ class NhAssistantMockAdmin {
118
139
  if (id !== undefined && action === 'feedback') {
119
140
  return this.agentFeedback(method, id, child, segments.length);
120
141
  }
142
+ if (id !== undefined && action === 'evaluation') {
143
+ return this.agentEvaluation(method, id, child, segments.length, query);
144
+ }
145
+ if (id !== undefined && (action === 'replay' || action === 'replays') && segments.length === 4) {
146
+ return this.agentReplay(method, id, action, body);
147
+ }
148
+ if (id !== undefined && action === 'skills' && child !== undefined && segments[5] === 'state' && segments.length === 6) {
149
+ return this.skillStateEndpoint(method, id, child, body);
150
+ }
121
151
  return this.agentsEndpoint(method, id, action, body, segments.length);
122
152
  case 'mcp-servers':
123
153
  return this.serversEndpoint(method, id, action, child, body, segments.length);
@@ -192,9 +222,22 @@ class NhAssistantMockAdmin {
192
222
  if (!stored.codeDefinition) {
193
223
  return failure(409, 'assistant-agent-not-code');
194
224
  }
195
- stored.agent = this.toAgent(stored.codeDefinition, 'code', stored.agent.version + 1, false);
225
+ stored.agent = this.toAgent(stored.codeDefinition, 'code', stored.agent.version + 1, false, stored.agent);
196
226
  return { status: 200, body: stored.agent };
197
227
  }
228
+ if (length === 4 && action === 'state') {
229
+ return this.agentStateEndpoint(stored, method, body);
230
+ }
231
+ if (length === 4 && action === 'kill-switch' && method === 'POST') {
232
+ const engaged = body?.engaged;
233
+ if (typeof engaged !== 'boolean') {
234
+ return validationFailure();
235
+ }
236
+ const changed = this.agentStateEndpoint(stored, 'POST', { state: engaged ? 'stopped' : 'active' });
237
+ return changed.status === 200
238
+ ? { status: 200, body: { agentId: id, enabled: stored.agent.runState !== 'stopped', cancelledRuns: 0 } }
239
+ : changed;
240
+ }
198
241
  if (length !== 3) {
199
242
  return failure(404, 'assistant-not-found');
200
243
  }
@@ -208,7 +251,7 @@ class NhAssistantMockAdmin {
208
251
  return failure(409, 'assistant-version-conflict');
209
252
  }
210
253
  const { expectedVersion, ...definition } = input;
211
- stored.agent = this.toAgent(definition, stored.agent.source, stored.agent.version + 1, stored.agent.source === 'code');
254
+ stored.agent = this.toAgent(definition, stored.agent.source, stored.agent.version + 1, stored.agent.source === 'code', stored.agent);
212
255
  return { status: 200, body: stored.agent };
213
256
  }
214
257
  if (method === 'DELETE') {
@@ -220,6 +263,68 @@ class NhAssistantMockAdmin {
220
263
  }
221
264
  return { status: 405 };
222
265
  }
266
+ /**
267
+ * `POST admin/agents/{id}/skills/{skillId}/state`: the skill's state, next to the agent's.
268
+ */
269
+ skillStateEndpoint(method, agentId, skillId, body) {
270
+ const stored = this.agents.get(agentId);
271
+ const skill = stored?.agent.skills?.find(item => item.skillId === skillId);
272
+ if (!stored || !skill) {
273
+ return failure(404, 'assistant-skill-not-found');
274
+ }
275
+ if (method !== 'POST') {
276
+ return { status: 405 };
277
+ }
278
+ const request = body;
279
+ if (request?.state !== 'active' && request?.state !== 'paused' && request?.state !== 'stopped') {
280
+ return validationFailure();
281
+ }
282
+ const reason = typeof request.reason === 'string' && request.reason.trim().length > 0 ? request.reason.trim() : null;
283
+ const changed = skill.runState === request.state
284
+ ? skill
285
+ : { ...skill, runState: request.state, changedAt: this.now(), changedBy: 'current-user', reason };
286
+ stored.agent = { ...stored.agent, skills: stored.agent.skills.map(item => item.skillId === skillId ? changed : item) };
287
+ return { status: 200, body: changed };
288
+ }
289
+ /**
290
+ * `GET` and `POST admin/agents/{id}/state`. The mock runs no agents on their own, so a pause
291
+ * collects no waiting events and a stop cancels no runs; the version never changes.
292
+ */
293
+ agentStateEndpoint(stored, method, body) {
294
+ if (method === 'GET') {
295
+ return { status: 200, body: toAgentState(stored.agent) };
296
+ }
297
+ if (method !== 'POST') {
298
+ return { status: 405 };
299
+ }
300
+ const request = body;
301
+ if (request?.state === undefined || request.state === null) {
302
+ return validationFailure();
303
+ }
304
+ if (request.state !== 'active' && request.state !== 'paused' && request.state !== 'stopped') {
305
+ return validationFailure();
306
+ }
307
+ const reason = typeof request.reason === 'string' && request.reason.trim().length > 0 ? request.reason.trim() : null;
308
+ if (reason !== null && reason.length > maxStateReason) {
309
+ return validationFailure();
310
+ }
311
+ if (stored.agent.runState === 'configured-off') {
312
+ return request.state === 'active'
313
+ ? failure(409, 'assistant-agent-configured-off')
314
+ : { status: 200, body: toAgentState(stored.agent) };
315
+ }
316
+ if (stored.agent.runState !== request.state) {
317
+ stored.agent = {
318
+ ...stored.agent,
319
+ isEnabled: request.state !== 'stopped',
320
+ runState: request.state,
321
+ stateChangedAt: this.now(),
322
+ stateChangedBy: 'current-user',
323
+ stateReason: reason
324
+ };
325
+ }
326
+ return { status: 200, body: toAgentState(stored.agent) };
327
+ }
223
328
  /**
224
329
  * The feedback report of an agent. The mock runs no agents on their own and records no proposals,
225
330
  * so the summary, the list and the JSON Lines export are empty.
@@ -244,6 +349,92 @@ class NhAssistantMockAdmin {
244
349
  }
245
350
  return failure(404, 'assistant-not-found');
246
351
  }
352
+ /**
353
+ * The comparison of a shadow variant with its live agent. The mock runs no agents on their own,
354
+ * so it pairs no runs and the export is empty.
355
+ */
356
+ agentEvaluation(method, agentId, child, length, query) {
357
+ if (!this.agents.has(agentId)) {
358
+ return failure(404, 'assistant-agent-not-found');
359
+ }
360
+ if (method !== 'GET') {
361
+ return { status: 405 };
362
+ }
363
+ const variantId = query.get('variant') ?? '';
364
+ if (this.agents.get(variantId)?.agent.shadowOf !== agentId) {
365
+ return failure(400, 'assistant-evaluation-not-variant');
366
+ }
367
+ if (length === 5 && child === 'export') {
368
+ return { status: 200 };
369
+ }
370
+ if (length !== 4) {
371
+ return failure(404, 'assistant-not-found');
372
+ }
373
+ const to = query.get('to') ?? this.now();
374
+ const from = query.get('from') ?? new Date(Date.parse(to) - 7 * 24 * 60 * 60 * 1000).toISOString();
375
+ const noCost = {
376
+ runs: 0, modelCalls: 0, tokens: 0, toolCalls: 0,
377
+ averageModelCalls: 0, averageTokens: 0, averageToolCalls: 0,
378
+ p95ModelCalls: 0, p95Tokens: 0, p95ToolCalls: 0
379
+ };
380
+ return {
381
+ status: 200,
382
+ body: {
383
+ agentId,
384
+ variantId,
385
+ from,
386
+ to,
387
+ runs: { live: 0, variant: 0, paired: 0, replayed: 0, liveUnpaired: 0, variantUnpaired: 0, truncated: false },
388
+ tools: [],
389
+ liveCost: noCost,
390
+ variantCost: noCost,
391
+ routing: [],
392
+ differences: []
393
+ }
394
+ };
395
+ }
396
+ /**
397
+ * Replays into a shadow variant. The mock records no runs, so a replay completes at once
398
+ * without replaying any.
399
+ */
400
+ agentReplay(method, agentId, action, body) {
401
+ const stored = this.agents.get(agentId);
402
+ if (!stored) {
403
+ return failure(404, 'assistant-agent-not-found');
404
+ }
405
+ if (action === 'replays') {
406
+ return method === 'GET' ? { status: 200, body: { items: [...(this.replays.get(agentId) ?? [])].reverse() } } : { status: 405 };
407
+ }
408
+ if (method !== 'POST') {
409
+ return { status: 405 };
410
+ }
411
+ if (!stored.agent.shadowOf) {
412
+ return failure(400, 'assistant-replay-not-shadow');
413
+ }
414
+ const request = (body ?? {});
415
+ const maxRuns = request.maxRuns ?? 100;
416
+ if (maxRuns < 1 || maxRuns > 500) {
417
+ return failure(400, 'assistant-message-invalid');
418
+ }
419
+ const now = this.now();
420
+ const to = request.to ?? now;
421
+ const replay = {
422
+ id: crypto.randomUUID(),
423
+ variantAgentId: agentId,
424
+ sourceAgentId: request.sourceAgentId ?? stored.agent.shadowOf,
425
+ from: request.from ?? new Date(Date.parse(to) - 7 * 24 * 60 * 60 * 1000).toISOString(),
426
+ to,
427
+ maxRuns,
428
+ status: 'completed',
429
+ stopReason: null,
430
+ runsReplayed: 0,
431
+ runsSkipped: 0,
432
+ createdAt: now,
433
+ endedAt: now
434
+ };
435
+ this.replays.set(agentId, [...(this.replays.get(agentId) ?? []), replay]);
436
+ return { status: 202, body: replay };
437
+ }
247
438
  serversEndpoint(method, id, action, child, body, length) {
248
439
  if (id === undefined) {
249
440
  if (method === 'GET') {
@@ -418,16 +609,29 @@ class NhAssistantMockAdmin {
418
609
  Array.isArray(input.mcpServerIds) && input.mcpServerIds.every(serverId => this.servers.has(serverId)) &&
419
610
  (input.requiredPolicy === null || (typeof input.requiredPolicy === 'string' && (!policies || policies.includes(input.requiredPolicy)))) &&
420
611
  autonomyLevels.includes(input.autonomy) &&
421
- typeof input.isEnabled === 'boolean';
612
+ (input.isEnabled === undefined || typeof input.isEnabled === 'boolean');
422
613
  }
423
- toAgent(input, source, version, isOverridden) {
614
+ /**
615
+ * The stored agent. `isEnabled` in the input is ignored, as by the server: the state comes from
616
+ * `previous`, and a new agent starts active.
617
+ */
618
+ toAgent(input, source, version, isOverridden, previous) {
619
+ const { isEnabled, ...definition } = structuredClone(input);
424
620
  return {
425
- ...structuredClone(input),
621
+ ...definition,
622
+ isEnabled: previous?.isEnabled ?? true,
426
623
  version,
427
624
  source,
428
625
  isOverridden,
429
626
  instructionsHash: hashText(input.instructions),
430
- updatedAt: this.now()
627
+ updatedAt: this.now(),
628
+ runState: previous?.runState ?? 'active',
629
+ stateChangedAt: previous?.stateChangedAt ?? null,
630
+ stateChangedBy: previous?.stateChangedBy ?? null,
631
+ stateReason: previous?.stateReason ?? null,
632
+ waitingInputs: previous?.waitingInputs ?? 0,
633
+ skills: previous?.skills ?? [],
634
+ shadowOf: previous?.shadowOf ?? null
431
635
  };
432
636
  }
433
637
  toServer(stored) {
@@ -449,8 +653,22 @@ function codeAgentFromSummary(agent) {
449
653
  toolSelectors: [],
450
654
  mcpServerIds: [],
451
655
  requiredPolicy: null,
452
- autonomy: agent.canMutate ? 'execute' : 'explain',
453
- isEnabled: true
656
+ autonomy: agent.canMutate ? 'execute' : 'explain'
657
+ };
658
+ }
659
+ function toAgentState(agent) {
660
+ return {
661
+ agentId: agent.id,
662
+ runState: agent.runState,
663
+ active: agent.runState === 'active',
664
+ changedAt: agent.stateChangedAt,
665
+ changedBy: agent.stateChangedBy,
666
+ reason: agent.stateReason,
667
+ waitingInputs: agent.waitingInputs,
668
+ cancelledRuns: 0,
669
+ startedRuns: 0,
670
+ skills: agent.skills ?? [],
671
+ shadowOf: agent.shadowOf ?? null
454
672
  };
455
673
  }
456
674
  function toServerFields(input) {
@@ -655,7 +873,7 @@ class NhAssistantMockBackend {
655
873
  return this.error(404, 'assistant-push-unavailable');
656
874
  }
657
875
  if (segments[0] === 'admin') {
658
- return this.result(this.admin.handleAdmin(method, segments, body));
876
+ return this.result(this.admin.handleAdmin(method, segments, body, query));
659
877
  }
660
878
  if (segments[0] !== 'conversations') {
661
879
  return this.empty(404);