@newheap/platform-ai-chat 0.20.0 → 0.21.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 +12 -2
- package/admin/i18n/en.json +1738 -0
- package/admin/i18n/nl.json +1738 -0
- package/admin/index.d.ts +47 -6
- package/fesm2022/newheap-platform-ai-chat-admin.mjs +3620 -23
- package/fesm2022/newheap-platform-ai-chat-admin.mjs.map +1 -1
- package/fesm2022/newheap-platform-ai-chat-testing.mjs +339 -19
- package/fesm2022/newheap-platform-ai-chat-testing.mjs.map +1 -1
- package/fesm2022/newheap-platform-ai-chat.mjs +145 -3488
- package/fesm2022/newheap-platform-ai-chat.mjs.map +1 -1
- package/i18n/en.json +4 -1726
- package/i18n/nl.json +4 -1726
- package/index.d.ts +97 -14
- package/package.json +4 -1
- package/testing/index.d.ts +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, inject, signal, Injectable, computed, DestroyRef, makeEnvironmentProviders } from '@angular/core';
|
|
3
|
-
import {
|
|
3
|
+
import { NH_ASSISTANT_AGENT_PACKAGE_FORMAT_VERSION, NH_ASSISTANT_AGENT_PACKAGE_FORMAT, NH_ASSISTANT_AGENT_PACKAGE_MAX_AGENTS, NH_ASSISTANT_PACKAGE_LOCK_ALL, NH_ASSISTANT_PACKAGE_LOCK_FIELDS, NH_ASSISTANT_AGENT_PACKAGE_MAX_BYTES, NH_ASSISTANT_UPLOAD_HEADER, ɵnhAssistantMediaTypeOf as _nhAssistantMediaTypeOf, NH_ASSISTANT_MAX_PROPOSAL_LOOKUP, NH_ASSISTANT_CONFIG, applyNhAssistantEvent, normalizeNhAssistantClientContext, applyNhAssistantApprovalDecision, applyNhAssistantQuestionAnswer, NH_ASSISTANT_FETCH, NH_ASSISTANT_UPLOAD, NhAssistantLiveService, NH_ASSISTANT_MCP_AUTHORIZATION_WINDOW } from '@newheap/platform-ai-chat';
|
|
4
4
|
import { Observable, Subject } from 'rxjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -399,6 +399,10 @@ function mockWorksOnErrors(worksOn, knownTypes, agentPolicies, divisions) {
|
|
|
399
399
|
}
|
|
400
400
|
/** Whether an agent works on subjects of the type, as the server's rule decides it. */
|
|
401
401
|
function mockWorksOnCovers(agent, subjectType) {
|
|
402
|
+
// A chat-only agent (`ChatOnly()` in code) works on no subject at all.
|
|
403
|
+
if (agent.chatOnly === true) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
402
406
|
if (subjectType === mockSystemSubjectType) {
|
|
403
407
|
return agent.worksOnApplication === true;
|
|
404
408
|
}
|
|
@@ -418,6 +422,8 @@ const consultationRoutes = [
|
|
|
418
422
|
const mockChatProfile = 'chat';
|
|
419
423
|
const autonomyLevels = ['observe', 'explain', 'propose', 'simulate', 'execute'];
|
|
420
424
|
const dataClassifications$1 = ['public', 'internal', 'confidential', 'restricted'];
|
|
425
|
+
/** The most MCP servers one agent is assigned to, as the server allows. */
|
|
426
|
+
const maxServersPerAgent = 32;
|
|
421
427
|
/**
|
|
422
428
|
* The agents of the mock administration: agents in code and agents created as data, their state,
|
|
423
429
|
* shadow modes, technical accounts, feedback, evaluation and replays. The mock runs no agents on
|
|
@@ -434,18 +440,24 @@ class NhAssistantMockAdminAgents {
|
|
|
434
440
|
const agents = admin.agents
|
|
435
441
|
?? scenario.agents.map(agent => ({ ...codeAgentFromSummary(agent), source: 'code' }));
|
|
436
442
|
// How an agent runs, in code or as data, belongs to its run configuration, not to the agent.
|
|
437
|
-
for (const { source, skills, shadowOf, shadow, serviceAccount, worksOnDeclared, knownSubjectTypes, consultations, consultationDescriptions, codeRunSettings: _, runConfiguration: __, consultableAsData: ___, dataConsultations: ____, consultationTools: _____, codeHandOffs: ______, ...input } of agents) {
|
|
443
|
+
for (const { source, skills, shadowOf, shadow, serviceAccount, worksOnDeclared, knownSubjectTypes, consultations, consultationDescriptions, chatOnly, codeRunSettings: _, runConfiguration: __, consultableAsData: ___, dataConsultations: ____, consultationTools: _____, codeHandOffs: ______, ...input } of agents) {
|
|
438
444
|
const variant = typeof shadowOf === 'string';
|
|
439
445
|
const agentShadow = variant || shadow === true;
|
|
446
|
+
// A chat-only agent has no shadow threads: it and its skills always work live.
|
|
447
|
+
const live = source === 'code' && chatOnly === true;
|
|
440
448
|
const initial = this.toAgent(input, source, 1, false);
|
|
441
449
|
this.agents.set(input.id, {
|
|
442
450
|
agent: {
|
|
443
451
|
...initial,
|
|
444
452
|
// Code says what its agents work on; a scenario agent that says nothing works on all objects.
|
|
445
453
|
...(source === 'code' ? { ...(mockWorksOnFromInput(input) ?? mockUndeclaredWorksOn), worksOnDeclared: worksOnDeclared ?? mockWorksOnFromInput(input) !== null } : {}),
|
|
454
|
+
// A chat-only agent in code (`ChatOnly()`) works on nothing, said on purpose.
|
|
455
|
+
...(source === 'code' && chatOnly === true
|
|
456
|
+
? { chatOnly: true, worksOnSubjectTypes: [], worksOnAllSubjects: false, worksOnApplication: false, perDivision: false, worksOnDeclared: true }
|
|
457
|
+
: {}),
|
|
446
458
|
knownSubjectTypes: knownSubjectTypes ?? [],
|
|
447
459
|
shadowOf: shadowOf ?? null,
|
|
448
|
-
shadow: shadowState(agentShadow, variant || shadow === true, null, variant),
|
|
460
|
+
shadow: shadowState(agentShadow, variant || shadow === true, null, variant || live),
|
|
449
461
|
serviceAccount: serviceAccount === undefined
|
|
450
462
|
? null
|
|
451
463
|
: { actorId: serviceAccount, configuredActorId: serviceAccount, overridden: false, changedAt: null, changedBy: null, reason: null },
|
|
@@ -463,7 +475,7 @@ class NhAssistantMockAdminAgents {
|
|
|
463
475
|
dailyTokens: null,
|
|
464
476
|
cancelledRuns: 0,
|
|
465
477
|
startedRuns: 0,
|
|
466
|
-
shadow: shadowState(agentShadow || skill.shadow === true, skill.shadow === true, null, variant)
|
|
478
|
+
shadow: shadowState(!live && (agentShadow || skill.shadow === true), skill.shadow === true, null, variant || live)
|
|
467
479
|
}))
|
|
468
480
|
},
|
|
469
481
|
codeDefinition: source === 'code' ? structuredClone(input) : null,
|
|
@@ -543,6 +555,29 @@ class NhAssistantMockAdminAgents {
|
|
|
543
555
|
live.agent = this.toAgent(definition, live.agent.source, live.agent.version + 1, live.agent.source === 'code', live.agent);
|
|
544
556
|
return live.agent;
|
|
545
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Assigns an MCP server to an agent or removes the assignment, as `PUT` and
|
|
560
|
+
* `DELETE admin/mcp-servers/{id}/agents/{agentId}` do: only the assignment changes, never the agent's
|
|
561
|
+
* version; an agent has at most 32 servers.
|
|
562
|
+
*/
|
|
563
|
+
setMcpAssignment(agentId, serverId, assign) {
|
|
564
|
+
const stored = this.agents.get(agentId);
|
|
565
|
+
if (!stored) {
|
|
566
|
+
return 'agent-not-found';
|
|
567
|
+
}
|
|
568
|
+
const assigned = stored.agent.mcpServerIds.includes(serverId);
|
|
569
|
+
if (assigned === assign) {
|
|
570
|
+
return 'unchanged';
|
|
571
|
+
}
|
|
572
|
+
if (assign && stored.agent.mcpServerIds.length >= maxServersPerAgent) {
|
|
573
|
+
return 'too-long';
|
|
574
|
+
}
|
|
575
|
+
stored.agent = {
|
|
576
|
+
...stored.agent,
|
|
577
|
+
mcpServerIds: assign ? [...stored.agent.mcpServerIds, serverId] : stored.agent.mcpServerIds.filter(id => id !== serverId)
|
|
578
|
+
};
|
|
579
|
+
return 'changed';
|
|
580
|
+
}
|
|
546
581
|
/** Removes a deleted MCP server from the agents it was assigned to. */
|
|
547
582
|
removeServer(serverId) {
|
|
548
583
|
for (const agent of this.agents.values()) {
|
|
@@ -600,7 +635,7 @@ class NhAssistantMockAdminAgents {
|
|
|
600
635
|
if (refused) {
|
|
601
636
|
return refused;
|
|
602
637
|
}
|
|
603
|
-
const created = this.toAgent(input, 'admin', 1, false);
|
|
638
|
+
const created = this.toAgent({ ...input, mcpServerIds: input.mcpServerIds ?? [] }, 'admin', 1, false);
|
|
604
639
|
// A data shadow variant always works in shadow and is never offered in the chat.
|
|
605
640
|
const agent = shadowOf === null
|
|
606
641
|
? created
|
|
@@ -660,7 +695,8 @@ class NhAssistantMockAdminAgents {
|
|
|
660
695
|
return mockFailure('assistant-version-conflict');
|
|
661
696
|
}
|
|
662
697
|
const { expectedVersion, ...definition } = input;
|
|
663
|
-
|
|
698
|
+
// An omitted list keeps the agent's assignments; an empty list removes them.
|
|
699
|
+
const updated = this.toAgent({ ...definition, mcpServerIds: definition.mcpServerIds ?? stored.agent.mcpServerIds }, stored.agent.source, stored.agent.version + 1, stored.agent.source === 'code', stored.agent);
|
|
664
700
|
// A data shadow variant keeps its live agent and stays out of the chat.
|
|
665
701
|
stored.agent = stored.agent.source === 'admin' && stored.agent.shadowOf ? { ...updated, offeredInChat: false } : updated;
|
|
666
702
|
return { status: 200, body: stored.agent };
|
|
@@ -714,6 +750,10 @@ class NhAssistantMockAdminAgents {
|
|
|
714
750
|
if (request === null) {
|
|
715
751
|
return mockValidationFailure();
|
|
716
752
|
}
|
|
753
|
+
// The skills of a chat-only agent always work live.
|
|
754
|
+
if (stored.agent.chatOnly === true) {
|
|
755
|
+
return mockFailure('assistant-agent-does-not-work-on');
|
|
756
|
+
}
|
|
717
757
|
const agentShadow = stored.agent.shadow ?? shadowState(false, false, null, false);
|
|
718
758
|
if (agentShadow.locked) {
|
|
719
759
|
return mockFailure('assistant-shadow-variant');
|
|
@@ -888,6 +928,10 @@ class NhAssistantMockAdminAgents {
|
|
|
888
928
|
if (request === null) {
|
|
889
929
|
return mockValidationFailure();
|
|
890
930
|
}
|
|
931
|
+
// A chat-only agent has no runs that could work in shadow threads.
|
|
932
|
+
if (stored.agent.chatOnly === true) {
|
|
933
|
+
return mockFailure('assistant-agent-does-not-work-on');
|
|
934
|
+
}
|
|
891
935
|
const current = stored.agent.shadow ?? shadowState(false, false, null, false);
|
|
892
936
|
if (current.locked) {
|
|
893
937
|
return mockFailure('assistant-shadow-variant');
|
|
@@ -1128,7 +1172,9 @@ class NhAssistantMockAdminAgents {
|
|
|
1128
1172
|
typeof input.description === 'string' &&
|
|
1129
1173
|
typeof input.instructions === 'string' && input.instructions.length <= mockMaxInstructions &&
|
|
1130
1174
|
Array.isArray(input.toolSelectors) && input.toolSelectors.every(selector => /^[a-z0-9*][a-z0-9.*-]*$/.test(selector)) &&
|
|
1131
|
-
|
|
1175
|
+
// Omitted, a new agent has no servers and an update keeps them, as on the server.
|
|
1176
|
+
(input.mcpServerIds === undefined || input.mcpServerIds === null ||
|
|
1177
|
+
(Array.isArray(input.mcpServerIds) && input.mcpServerIds.every(serverId => this.rules.hasServer(serverId)))) &&
|
|
1132
1178
|
(input.requiredPolicy === null || (typeof input.requiredPolicy === 'string' && (!policies || policies.includes(input.requiredPolicy)))) &&
|
|
1133
1179
|
autonomyLevels.includes(input.autonomy) &&
|
|
1134
1180
|
(input.isEnabled === undefined || typeof input.isEnabled === 'boolean');
|
|
@@ -1151,6 +1197,8 @@ class NhAssistantMockAdminAgents {
|
|
|
1151
1197
|
const classification = chosenClassification(dataClassification);
|
|
1152
1198
|
return this.withWarnings({
|
|
1153
1199
|
...definition,
|
|
1200
|
+
// The callers name the servers; an omitted list keeps those of the agent, or none.
|
|
1201
|
+
mcpServerIds: definition.mcpServerIds ?? previous?.mcpServerIds ?? [],
|
|
1154
1202
|
// An agent created here keeps its profile when the input omits it; code decides for the others.
|
|
1155
1203
|
profile: chosenProfile ?? previous?.profile ?? mockChatProfile,
|
|
1156
1204
|
// The same for its classification; an agent in code keeps the one of its scenario.
|
|
@@ -1317,6 +1365,9 @@ const readerPriority = { min: -1000, max: 1000 };
|
|
|
1317
1365
|
const classifications$1 = ['public', 'internal', 'confidential', 'restricted'];
|
|
1318
1366
|
const mediaTypePattern$1 = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/;
|
|
1319
1367
|
const controlCharacters = /[\u0000-\u001f\u007f]/;
|
|
1368
|
+
/** The longest remote tool name a sync stores, and the most tools of a server that wait for a sync. */
|
|
1369
|
+
const maxRemoteName = 128;
|
|
1370
|
+
const maxToolsAwaitingSync = 100;
|
|
1320
1371
|
/**
|
|
1321
1372
|
* The MCP servers of the mock administration and the caller's own connections: secrets are
|
|
1322
1373
|
* write-only, new tools start disabled as mutations, a changed remote schema disables a tool, OAuth
|
|
@@ -1744,19 +1795,20 @@ class NhAssistantMockAdminMcp {
|
|
|
1744
1795
|
if (length === 5 && action === 'tools' && method === 'PUT' && child !== undefined) {
|
|
1745
1796
|
const tool = stored.tools.get(child);
|
|
1746
1797
|
const input = body;
|
|
1747
|
-
if (!tool) {
|
|
1748
|
-
return mockFailure('assistant-mcp-tool-not-found');
|
|
1749
|
-
}
|
|
1750
1798
|
if (typeof input?.isEnabled !== 'boolean' || (input.effect !== 'read-only' && input.effect !== 'mutation') ||
|
|
1751
1799
|
(input.descriptionOverride !== null && typeof input.descriptionOverride !== 'string')) {
|
|
1752
1800
|
return mockValidationFailure();
|
|
1753
1801
|
}
|
|
1754
|
-
if (input.isEnabled && tool.status === 'missing') {
|
|
1755
|
-
return mockValidationFailure('isEnabled');
|
|
1756
|
-
}
|
|
1757
1802
|
const override = typeof input.descriptionOverride === 'string' && input.descriptionOverride.trim().length > 0
|
|
1758
1803
|
? input.descriptionOverride.slice(0, 1_000)
|
|
1759
1804
|
: null;
|
|
1805
|
+
if (!tool) {
|
|
1806
|
+
return this.reviewBeforeSync(stored, child, input.isEnabled, input.effect, override);
|
|
1807
|
+
}
|
|
1808
|
+
// A tool the server no longer lists cannot be enabled; one that no sync has seen yet can.
|
|
1809
|
+
if (input.isEnabled && tool.status === 'missing' && !tool.awaitingSync) {
|
|
1810
|
+
return mockFailure('assistant-mcp-tool-disabled');
|
|
1811
|
+
}
|
|
1760
1812
|
const updated = {
|
|
1761
1813
|
...tool,
|
|
1762
1814
|
isEnabled: input.isEnabled,
|
|
@@ -1767,14 +1819,77 @@ class NhAssistantMockAdminMcp {
|
|
|
1767
1819
|
stored.tools.set(child, updated);
|
|
1768
1820
|
return { status: 200, body: toTool(updated) };
|
|
1769
1821
|
}
|
|
1822
|
+
if (length === 5 && action === 'agents' && child !== undefined) {
|
|
1823
|
+
if (method !== 'PUT' && method !== 'DELETE') {
|
|
1824
|
+
return { status: 405 };
|
|
1825
|
+
}
|
|
1826
|
+
// Only the assignment changes, never the agent's version, as on the server.
|
|
1827
|
+
const changed = this.context.setAssignment(stored.server.id, child, method === 'PUT');
|
|
1828
|
+
if (changed === 'agent-not-found') {
|
|
1829
|
+
return mockFailure('assistant-agent-not-found');
|
|
1830
|
+
}
|
|
1831
|
+
if (changed === 'too-long') {
|
|
1832
|
+
return mockFailure('assistant-validation', { mcpServerIds: ['too-long'] });
|
|
1833
|
+
}
|
|
1834
|
+
return { status: 200, body: this.toServer(stored) };
|
|
1835
|
+
}
|
|
1770
1836
|
return mockFailure('assistant-not-found');
|
|
1771
1837
|
}
|
|
1838
|
+
/**
|
|
1839
|
+
* The reviewed settings of a tool that no sync has seen yet, as the server stores them: a missing tool
|
|
1840
|
+
* without a schema that waits for its sync, with a name a sync could store and no other spelling of
|
|
1841
|
+
* a stored tool's name; at most 100 per server.
|
|
1842
|
+
*/
|
|
1843
|
+
reviewBeforeSync(stored, remoteName, isEnabled, effect, descriptionOverride) {
|
|
1844
|
+
if (remoteName.trim().length === 0) {
|
|
1845
|
+
return mockFailure('assistant-validation', { remoteName: ['required'] });
|
|
1846
|
+
}
|
|
1847
|
+
if (remoteName.length > maxRemoteName) {
|
|
1848
|
+
return mockFailure('assistant-validation', { remoteName: ['too-long'] });
|
|
1849
|
+
}
|
|
1850
|
+
const lower = remoteName.toLowerCase();
|
|
1851
|
+
if (controlCharacters.test(remoteName) || [...stored.tools.keys()].some(name => name.toLowerCase() === lower)) {
|
|
1852
|
+
return mockFailure('assistant-validation', { remoteName: ['invalid'] });
|
|
1853
|
+
}
|
|
1854
|
+
if ([...stored.tools.values()].filter(tool => tool.awaitingSync).length >= maxToolsAwaitingSync) {
|
|
1855
|
+
return mockFailure('assistant-configuration-limit');
|
|
1856
|
+
}
|
|
1857
|
+
const tool = {
|
|
1858
|
+
remoteName,
|
|
1859
|
+
localId: `mcp.${stored.server.id}.${kebab(remoteName)}`,
|
|
1860
|
+
description: '',
|
|
1861
|
+
descriptionOverride,
|
|
1862
|
+
isEnabled,
|
|
1863
|
+
effect,
|
|
1864
|
+
status: 'missing',
|
|
1865
|
+
readOnlyHint: null,
|
|
1866
|
+
awaitingSync: true,
|
|
1867
|
+
inputSchemaHash: ''
|
|
1868
|
+
};
|
|
1869
|
+
stored.tools.set(remoteName, tool);
|
|
1870
|
+
return { status: 200, body: toTool(tool) };
|
|
1871
|
+
}
|
|
1772
1872
|
sync(stored) {
|
|
1773
1873
|
const seen = new Set();
|
|
1774
1874
|
for (const remote of stored.remoteTools) {
|
|
1775
1875
|
seen.add(remote.remoteName);
|
|
1776
|
-
const existing = stored.tools.get(remote.remoteName);
|
|
1876
|
+
const existing = stored.tools.get(remote.remoteName) ?? this.reviewedInOtherSpelling(stored, remote.remoteName);
|
|
1777
1877
|
const description = remote.description.slice(0, 1_000);
|
|
1878
|
+
if (existing?.awaitingSync) {
|
|
1879
|
+
// The first sync that sees a reviewed tool keeps its settings and takes the server's spelling.
|
|
1880
|
+
stored.tools.delete(existing.remoteName);
|
|
1881
|
+
stored.tools.set(remote.remoteName, {
|
|
1882
|
+
...existing,
|
|
1883
|
+
remoteName: remote.remoteName,
|
|
1884
|
+
localId: `mcp.${stored.server.id}.${kebab(remote.remoteName)}`,
|
|
1885
|
+
description,
|
|
1886
|
+
status: 'available',
|
|
1887
|
+
readOnlyHint: remote.readOnlyHint ?? null,
|
|
1888
|
+
awaitingSync: false,
|
|
1889
|
+
inputSchemaHash: remote.inputSchemaHash
|
|
1890
|
+
});
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1778
1893
|
if (!existing) {
|
|
1779
1894
|
stored.tools.set(remote.remoteName, {
|
|
1780
1895
|
remoteName: remote.remoteName,
|
|
@@ -1800,11 +1915,19 @@ class NhAssistantMockAdminMcp {
|
|
|
1800
1915
|
});
|
|
1801
1916
|
}
|
|
1802
1917
|
for (const [name, tool] of stored.tools) {
|
|
1803
|
-
|
|
1918
|
+
// A reviewed tool that this sync does not see keeps waiting with its settings.
|
|
1919
|
+
if (!seen.has(name) && !tool.awaitingSync) {
|
|
1804
1920
|
stored.tools.set(name, { ...tool, isEnabled: false, status: 'missing' });
|
|
1805
1921
|
}
|
|
1806
1922
|
}
|
|
1807
1923
|
}
|
|
1924
|
+
/** A reviewed tool that waits for its sync under another spelling of a name no remote tool has. */
|
|
1925
|
+
reviewedInOtherSpelling(stored, remoteName) {
|
|
1926
|
+
const lower = remoteName.toLowerCase();
|
|
1927
|
+
const names = new Set(stored.remoteTools.map(remote => remote.remoteName));
|
|
1928
|
+
return [...stored.tools.values()]
|
|
1929
|
+
.find(tool => tool.awaitingSync && !names.has(tool.remoteName) && tool.remoteName.toLowerCase() === lower);
|
|
1930
|
+
}
|
|
1808
1931
|
/**
|
|
1809
1932
|
* The field errors of a server change, as the server reports them in one refusal: the fields
|
|
1810
1933
|
* themselves, the credential and the OAuth settings. Empty when the change is valid.
|
|
@@ -2482,7 +2605,7 @@ function allowedServerUrl(value) {
|
|
|
2482
2605
|
}
|
|
2483
2606
|
}
|
|
2484
2607
|
function toTool({ inputSchemaHash, ...tool }) {
|
|
2485
|
-
return tool;
|
|
2608
|
+
return { ...tool, awaitingSync: tool.awaitingSync === true };
|
|
2486
2609
|
}
|
|
2487
2610
|
function toTools(stored) {
|
|
2488
2611
|
return [...stored.tools.values()].map(toTool);
|
|
@@ -2789,7 +2912,9 @@ class NhAssistantMockHandOffs {
|
|
|
2789
2912
|
...data.map(agent => {
|
|
2790
2913
|
const hasRunSettings = this.agents.isConfigured(agent);
|
|
2791
2914
|
return {
|
|
2792
|
-
agentId: agent.id, displayName: agent.displayName, source: 'data', hasRunSettings, inEffect: hasRunSettings, problem: null, version: agent.version
|
|
2915
|
+
agentId: agent.id, displayName: agent.displayName, source: 'data', hasRunSettings, inEffect: hasRunSettings, problem: null, version: agent.version,
|
|
2916
|
+
packageId: null,
|
|
2917
|
+
packageVersion: null
|
|
2793
2918
|
};
|
|
2794
2919
|
})
|
|
2795
2920
|
]
|
|
@@ -4435,6 +4560,7 @@ class NhAssistantMockAdmin {
|
|
|
4435
4560
|
agents: () => this.agents.all(),
|
|
4436
4561
|
chatAgentIds: () => this.agents.chatAgents().map(agent => agent.id),
|
|
4437
4562
|
removeServer: serverId => this.agents.removeServer(serverId),
|
|
4563
|
+
setAssignment: (serverId, agentId, assign) => this.agents.setMcpAssignment(agentId, serverId, assign),
|
|
4438
4564
|
canAdminister: () => this.canAdminister
|
|
4439
4565
|
});
|
|
4440
4566
|
this.presetRunConfigurations(admin.agents ?? []);
|
|
@@ -4866,8 +4992,139 @@ class NhAssistantMockAgentPackages {
|
|
|
4866
4992
|
if (segments[1] === 'agents' && segments[3] === 'package' && segments[4] === 'unlink' && segments.length === 5) {
|
|
4867
4993
|
return method === 'POST' ? this.unlink(segments[2], body) : { status: 405 };
|
|
4868
4994
|
}
|
|
4995
|
+
// A promotion as the package's version is an import of the variant's parts onto its live agent.
|
|
4996
|
+
if (segments[1] === 'agents' && segments[3] === 'promote' && segments.length === 4
|
|
4997
|
+
&& body?.asPackageVersion === true) {
|
|
4998
|
+
return method === 'POST' ? this.promoteAsPackageVersion(segments[2], body) : { status: 405 };
|
|
4999
|
+
}
|
|
4869
5000
|
return null;
|
|
4870
5001
|
}
|
|
5002
|
+
/**
|
|
5003
|
+
* `POST admin/agents/{id}/promote` with `asPackageVersion` (FB11): a data shadow variant whose every
|
|
5004
|
+
* part is linked to one package becomes that package's next version of its live agent. Its parts,
|
|
5005
|
+
* as an export reads them, are imported onto the live agent with the import's rules and relinked;
|
|
5006
|
+
* the live agent keeps its name and chat offer, an agent in code its run settings in code. The
|
|
5007
|
+
* variant leaves first, with its links, so its trigger ids are free, and the live agent leaves the
|
|
5008
|
+
* package it belonged to; a refused part puts everything back and answers the import's
|
|
5009
|
+
* `409 assistant-package-refused`. A part without a link answers `404 assistant-package-not-linked`.
|
|
5010
|
+
*/
|
|
5011
|
+
promoteAsPackageVersion(variantId, body) {
|
|
5012
|
+
const listed = this.context.call('GET', 'admin/agents');
|
|
5013
|
+
if (listed.status !== 200) {
|
|
5014
|
+
return listed;
|
|
5015
|
+
}
|
|
5016
|
+
const request = (body ?? {});
|
|
5017
|
+
if (typeof request.expectedVersion !== 'number') {
|
|
5018
|
+
return mockFailure('assistant-validation', { expectedVersion: ['required'] });
|
|
5019
|
+
}
|
|
5020
|
+
if (request.moveSkills === true) {
|
|
5021
|
+
return mockFailure('assistant-validation', { moveSkills: ['not-allowed'] });
|
|
5022
|
+
}
|
|
5023
|
+
const variantRead = this.read(variantId);
|
|
5024
|
+
if (!variantRead) {
|
|
5025
|
+
return mockFailure('assistant-agent-not-found');
|
|
5026
|
+
}
|
|
5027
|
+
const variant = variantRead.agent;
|
|
5028
|
+
if (variant.source !== 'admin' || !variant.shadowOf) {
|
|
5029
|
+
return mockFailure('assistant-not-shadow-variant');
|
|
5030
|
+
}
|
|
5031
|
+
const liveRead = this.read(variant.shadowOf);
|
|
5032
|
+
if (!liveRead) {
|
|
5033
|
+
return mockFailure('assistant-agent-not-found');
|
|
5034
|
+
}
|
|
5035
|
+
const live = liveRead.agent;
|
|
5036
|
+
if (request.expectedVersion !== live.version
|
|
5037
|
+
|| (typeof request.expectedVariantVersion === 'number' && request.expectedVariantVersion !== variant.version)) {
|
|
5038
|
+
return mockFailure('assistant-version-conflict');
|
|
5039
|
+
}
|
|
5040
|
+
// Every part of the variant is linked to its package.
|
|
5041
|
+
const link = this.links.get(variantId);
|
|
5042
|
+
const parts = this.packageAgentOf(variantRead);
|
|
5043
|
+
if (!link || [...partsOf(parts).keys()].some(part => !link.parts.has(part))) {
|
|
5044
|
+
return mockFailure('assistant-package-not-linked');
|
|
5045
|
+
}
|
|
5046
|
+
const inCode = live.source === 'code';
|
|
5047
|
+
const carried = parts.definition;
|
|
5048
|
+
const definition = !carried
|
|
5049
|
+
? null
|
|
5050
|
+
: inCode
|
|
5051
|
+
? {
|
|
5052
|
+
description: carried.description,
|
|
5053
|
+
instructions: carried.instructions,
|
|
5054
|
+
toolSelectors: carried.toolSelectors,
|
|
5055
|
+
requiredPolicy: carried.requiredPolicy,
|
|
5056
|
+
autonomy: carried.autonomy
|
|
5057
|
+
}
|
|
5058
|
+
: { ...carried, displayName: live.displayName, offeredInChat: live.offeredInChat !== false, shadowOf: null, startsOff: null, startsInShadow: null };
|
|
5059
|
+
const locks = Object.fromEntries([...link.parts]
|
|
5060
|
+
.filter(([part, partLink]) => partLink.lockedFields.length > 0 && (part !== 'runSettings' || !inCode))
|
|
5061
|
+
.map(([part, partLink]) => [part, [...partLink.lockedFields]]));
|
|
5062
|
+
const agent = withoutNulls({
|
|
5063
|
+
...parts,
|
|
5064
|
+
id: live.id,
|
|
5065
|
+
source: live.source,
|
|
5066
|
+
definition,
|
|
5067
|
+
runSettings: inCode ? null : parts.runSettings,
|
|
5068
|
+
groups: null,
|
|
5069
|
+
hashes: null,
|
|
5070
|
+
locks: Object.keys(locks).length > 0 ? locks : null
|
|
5071
|
+
});
|
|
5072
|
+
const agentPackage = {
|
|
5073
|
+
format: NH_ASSISTANT_AGENT_PACKAGE_FORMAT,
|
|
5074
|
+
formatVersion: NH_ASSISTANT_AGENT_PACKAGE_FORMAT_VERSION,
|
|
5075
|
+
package: { id: link.packageId, version: link.packageVersion },
|
|
5076
|
+
agents: [agent]
|
|
5077
|
+
};
|
|
5078
|
+
// The live agent's technical account stays; without one it takes the variant's.
|
|
5079
|
+
const account = liveRead.configuration?.runSettings?.serviceAccountActorId
|
|
5080
|
+
?? variantRead.configuration?.runSettings?.serviceAccountActorId
|
|
5081
|
+
?? null;
|
|
5082
|
+
// All of it or nothing: the variant leaves first, and the package's other agents stay linked.
|
|
5083
|
+
const restore = [this.context.snapshot(), this.snapshot()];
|
|
5084
|
+
const others = [...this.links].filter(([agentId, item]) => item.packageId === link.packageId && agentId !== variantId);
|
|
5085
|
+
this.links.delete(variantId);
|
|
5086
|
+
const deleted = this.context.call('DELETE', `admin/agents/${encodeURIComponent(variantId)}`);
|
|
5087
|
+
if (deleted.status >= 300) {
|
|
5088
|
+
restore.forEach(put => put());
|
|
5089
|
+
return deleted;
|
|
5090
|
+
}
|
|
5091
|
+
for (const [agentId] of others) {
|
|
5092
|
+
this.links.delete(agentId);
|
|
5093
|
+
}
|
|
5094
|
+
this.links.delete(live.id);
|
|
5095
|
+
for (const installed of this.installed.values()) {
|
|
5096
|
+
installed.agents = installed.agents.filter(agentId => agentId !== live.id && agentId !== variantId);
|
|
5097
|
+
}
|
|
5098
|
+
const imported = this.importPackage({
|
|
5099
|
+
package: agentPackage,
|
|
5100
|
+
options: { relink: [{ agentId: live.id }], serviceAccounts: account ? { [live.id]: account } : null }
|
|
5101
|
+
}, true);
|
|
5102
|
+
if (imported.status !== 200) {
|
|
5103
|
+
restore.forEach(put => put());
|
|
5104
|
+
return imported;
|
|
5105
|
+
}
|
|
5106
|
+
for (const [agentId, item] of others) {
|
|
5107
|
+
this.links.set(agentId, item);
|
|
5108
|
+
}
|
|
5109
|
+
this.installed.set(link.packageId, {
|
|
5110
|
+
version: link.packageVersion,
|
|
5111
|
+
agents: [...others.map(([agentId]) => agentId), live.id].sort()
|
|
5112
|
+
});
|
|
5113
|
+
const result = imported.body;
|
|
5114
|
+
const promoted = this.read(live.id).agent;
|
|
5115
|
+
return {
|
|
5116
|
+
status: 200,
|
|
5117
|
+
body: {
|
|
5118
|
+
agent: this.withAgentPackage(promoted),
|
|
5119
|
+
variantId,
|
|
5120
|
+
movedSkills: result.parts
|
|
5121
|
+
.filter(part => part.part.startsWith('skill:') && part.outcome !== 'refused' && part.outcome !== 'left-behind' && part.outcome !== 'kept-local')
|
|
5122
|
+
.map(part => part.part.slice('skill:'.length))
|
|
5123
|
+
.sort(),
|
|
5124
|
+
package: result
|
|
5125
|
+
}
|
|
5126
|
+
};
|
|
5127
|
+
}
|
|
4871
5128
|
/**
|
|
4872
5129
|
* Runs a save of the administration under the package locks: a save may keep a locked field's value
|
|
4873
5130
|
* or take the package's, never another; such a change is undone and refused with `{field}: locked`.
|
|
@@ -4975,6 +5232,22 @@ class NhAssistantMockAgentPackages {
|
|
|
4975
5232
|
return { ...result, body: this.withLink(agentId, `metadata:${id}`, body) };
|
|
4976
5233
|
}
|
|
4977
5234
|
return { ...result, body: body.map(entry => this.withLink(agentId, `metadata:${entry.name}`, entry)) };
|
|
5235
|
+
case 'shadow-variants': {
|
|
5236
|
+
// A data variant names the package its parts are linked to.
|
|
5237
|
+
const listing = body;
|
|
5238
|
+
return {
|
|
5239
|
+
...result,
|
|
5240
|
+
body: {
|
|
5241
|
+
...listing,
|
|
5242
|
+
variants: listing.variants.map(variant => {
|
|
5243
|
+
const link = this.links.get(variant.agentId);
|
|
5244
|
+
return variant.source === 'data' && link && link.parts.size > 0
|
|
5245
|
+
? { ...variant, packageId: link.packageId, packageVersion: link.packageVersion }
|
|
5246
|
+
: variant;
|
|
5247
|
+
})
|
|
5248
|
+
}
|
|
5249
|
+
};
|
|
5250
|
+
}
|
|
4978
5251
|
default:
|
|
4979
5252
|
return result;
|
|
4980
5253
|
}
|
|
@@ -7964,7 +8237,8 @@ class NhAssistantMockProposals {
|
|
|
7964
8237
|
parent: item.parent ?? null,
|
|
7965
8238
|
canGiveFeedback: false,
|
|
7966
8239
|
consultationId: item.consultationId ?? null,
|
|
7967
|
-
consultationRoute: route
|
|
8240
|
+
consultationRoute: route,
|
|
8241
|
+
...(item.supersededByApprovalId ? { supersededByApprovalId: item.supersededByApprovalId } : {})
|
|
7968
8242
|
},
|
|
7969
8243
|
divisionId: item.divisionId ?? null,
|
|
7970
8244
|
canDecide,
|
|
@@ -8830,7 +9104,10 @@ class NhAssistantMockBackend {
|
|
|
8830
9104
|
if (filesCode) {
|
|
8831
9105
|
return this.error(filesCode);
|
|
8832
9106
|
}
|
|
8833
|
-
|
|
9107
|
+
// Like the server: a question or an approval that passed its lifetime closes as expired, and the
|
|
9108
|
+
// message starts its turn (CS9).
|
|
9109
|
+
this.closeExpiredWait(conversation);
|
|
9110
|
+
if (!acceptsMessage(this.conversations.get(conversation.id)?.status ?? conversation.status)) {
|
|
8834
9111
|
return this.empty(409);
|
|
8835
9112
|
}
|
|
8836
9113
|
// Like the server: an invalid page context is dropped, never a 400.
|
|
@@ -9005,6 +9282,28 @@ class NhAssistantMockBackend {
|
|
|
9005
9282
|
await this.playSteps(conversation.id, assistantMessageId, steps, emit);
|
|
9006
9283
|
});
|
|
9007
9284
|
}
|
|
9285
|
+
/**
|
|
9286
|
+
* Closes the question or the approval a chat waits for once its lifetime passed, as a late answer or
|
|
9287
|
+
* decision closes it on the server; one that is still valid keeps the chat busy. Agent threads keep
|
|
9288
|
+
* their own rules.
|
|
9289
|
+
*/
|
|
9290
|
+
closeExpiredWait(conversation) {
|
|
9291
|
+
if (conversation.status !== 'waiting-for-approval' || conversation.kind === 'thread') {
|
|
9292
|
+
return;
|
|
9293
|
+
}
|
|
9294
|
+
const now = Date.now();
|
|
9295
|
+
const question = this.pendingQuestions.get(conversation.id);
|
|
9296
|
+
if (question && Date.parse(question.question.expiresAt) <= now) {
|
|
9297
|
+
this.pendingQuestions.delete(conversation.id);
|
|
9298
|
+
this.update(conversation.id, current => withClosedQuestion(current, question.question.questionId, 'expired'));
|
|
9299
|
+
return;
|
|
9300
|
+
}
|
|
9301
|
+
const pending = this.pendingApprovals.get(conversation.id);
|
|
9302
|
+
if (pending && Date.parse(pending.approval.expiresAt) <= now) {
|
|
9303
|
+
this.pendingApprovals.delete(conversation.id);
|
|
9304
|
+
this.update(conversation.id, current => withExpiredApproval(current, pending.approval.approvalId, pending.invocationId));
|
|
9305
|
+
}
|
|
9306
|
+
}
|
|
9008
9307
|
cancel(conversation) {
|
|
9009
9308
|
const run = this.runs.get(conversation.id);
|
|
9010
9309
|
if (run) {
|
|
@@ -9589,6 +9888,27 @@ function withClosedQuestion(conversation, questionId, status) {
|
|
|
9589
9888
|
messages: conversation.messages.map(message => ({ ...message, parts: message.parts.map(closed) }))
|
|
9590
9889
|
};
|
|
9591
9890
|
}
|
|
9891
|
+
/**
|
|
9892
|
+
* An approval that passed its lifetime, as the server closes it when the next message arrives: it
|
|
9893
|
+
* expired, its call failed with `assistant-approval-expired`, and the conversation takes the message.
|
|
9894
|
+
*/
|
|
9895
|
+
function withExpiredApproval(conversation, approvalId, invocationId) {
|
|
9896
|
+
const expired = (part) => {
|
|
9897
|
+
if (part.type === 'approval' && part.approvalId === approvalId) {
|
|
9898
|
+
return { ...part, status: 'expired' };
|
|
9899
|
+
}
|
|
9900
|
+
if (part.type === 'tool-call' && part.invocationId === invocationId) {
|
|
9901
|
+
return { ...part, status: 'failed', resultCode: 'assistant-approval-expired' };
|
|
9902
|
+
}
|
|
9903
|
+
return part;
|
|
9904
|
+
};
|
|
9905
|
+
return {
|
|
9906
|
+
...conversation,
|
|
9907
|
+
status: 'idle',
|
|
9908
|
+
pendingApproval: null,
|
|
9909
|
+
messages: conversation.messages.map(message => ({ ...message, parts: message.parts.map(expired) }))
|
|
9910
|
+
};
|
|
9911
|
+
}
|
|
9592
9912
|
/** Like the server: a new message starts in an idle conversation or after a failed turn. */
|
|
9593
9913
|
function acceptsMessage(status) {
|
|
9594
9914
|
return status === 'idle' || status === 'failed';
|