@inspectr/mcplab 1.29.0 → 1.31.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.
Files changed (126) hide show
  1. package/README.md +24 -3
  2. package/dist/app/assets/index-Cwogc_kk.js +267 -0
  3. package/dist/app/assets/index-D5OYchKq.css +2 -0
  4. package/dist/app/index.html +2 -2
  5. package/dist/app-server/app-context.d.ts +6 -22
  6. package/dist/app-server/app-context.d.ts.map +1 -1
  7. package/dist/app-server/assistant-agent.d.ts +10 -0
  8. package/dist/app-server/assistant-agent.d.ts.map +1 -0
  9. package/dist/app-server/assistant-agent.js +29 -0
  10. package/dist/app-server/assistant-agent.js.map +1 -0
  11. package/dist/app-server/assistant-common.d.ts +7 -34
  12. package/dist/app-server/assistant-common.d.ts.map +1 -1
  13. package/dist/app-server/assistant-common.js +33 -194
  14. package/dist/app-server/assistant-common.js.map +1 -1
  15. package/dist/app-server/assistant-events.d.ts +10 -6
  16. package/dist/app-server/assistant-events.d.ts.map +1 -1
  17. package/dist/app-server/assistant-events.js +23 -11
  18. package/dist/app-server/assistant-events.js.map +1 -1
  19. package/dist/app-server/assistant-graph-nodes.d.ts +226 -0
  20. package/dist/app-server/assistant-graph-nodes.d.ts.map +1 -0
  21. package/dist/app-server/assistant-graph-nodes.js +365 -0
  22. package/dist/app-server/assistant-graph-nodes.js.map +1 -0
  23. package/dist/app-server/assistant-graph-state.d.ts +64 -0
  24. package/dist/app-server/assistant-graph-state.d.ts.map +1 -0
  25. package/dist/app-server/assistant-graph-state.js +137 -0
  26. package/dist/app-server/assistant-graph-state.js.map +1 -0
  27. package/dist/app-server/assistant-mcp-context-tools.d.ts +17 -0
  28. package/dist/app-server/assistant-mcp-context-tools.d.ts.map +1 -0
  29. package/dist/app-server/assistant-mcp-context-tools.js +149 -0
  30. package/dist/app-server/assistant-mcp-context-tools.js.map +1 -0
  31. package/dist/app-server/assistant-model.d.ts +24 -0
  32. package/dist/app-server/assistant-model.d.ts.map +1 -0
  33. package/dist/app-server/assistant-model.js +240 -0
  34. package/dist/app-server/assistant-model.js.map +1 -0
  35. package/dist/app-server/assistant-runtime-contract.d.ts +46 -0
  36. package/dist/app-server/assistant-runtime-contract.d.ts.map +1 -0
  37. package/dist/app-server/assistant-runtime-contract.js +2 -0
  38. package/dist/app-server/assistant-runtime-contract.js.map +1 -0
  39. package/dist/app-server/assistant-runtime-policy.d.ts +22 -0
  40. package/dist/app-server/assistant-runtime-policy.d.ts.map +1 -0
  41. package/dist/app-server/assistant-runtime-policy.js +71 -0
  42. package/dist/app-server/assistant-runtime-policy.js.map +1 -0
  43. package/dist/app-server/assistant-service.d.ts +23 -0
  44. package/dist/app-server/assistant-service.d.ts.map +1 -0
  45. package/dist/app-server/assistant-service.js +185 -0
  46. package/dist/app-server/assistant-service.js.map +1 -0
  47. package/dist/app-server/assistant-session-routes.d.ts +25 -0
  48. package/dist/app-server/assistant-session-routes.d.ts.map +1 -0
  49. package/dist/app-server/assistant-session-routes.js +210 -0
  50. package/dist/app-server/assistant-session-routes.js.map +1 -0
  51. package/dist/app-server/assistant-tracing.d.ts +5 -0
  52. package/dist/app-server/assistant-tracing.d.ts.map +1 -0
  53. package/dist/app-server/assistant-tracing.js +30 -0
  54. package/dist/app-server/assistant-tracing.js.map +1 -0
  55. package/dist/app-server/browser-provider-learning.js +1 -1
  56. package/dist/app-server/browser-provider-learning.js.map +1 -1
  57. package/dist/app-server/connection-test-routes.d.ts +8 -0
  58. package/dist/app-server/connection-test-routes.d.ts.map +1 -0
  59. package/dist/app-server/connection-test-routes.js +34 -0
  60. package/dist/app-server/connection-test-routes.js.map +1 -0
  61. package/dist/app-server/decision-routes.d.ts +8 -0
  62. package/dist/app-server/decision-routes.d.ts.map +1 -0
  63. package/dist/app-server/decision-routes.js +27 -0
  64. package/dist/app-server/decision-routes.js.map +1 -0
  65. package/dist/app-server/langchain-assistant-runtime.d.ts +11 -0
  66. package/dist/app-server/langchain-assistant-runtime.d.ts.map +1 -0
  67. package/dist/app-server/langchain-assistant-runtime.js +852 -0
  68. package/dist/app-server/langchain-assistant-runtime.js.map +1 -0
  69. package/dist/app-server/live-tests.d.ts +2 -5
  70. package/dist/app-server/live-tests.d.ts.map +1 -1
  71. package/dist/app-server/live-tests.js +21 -8
  72. package/dist/app-server/live-tests.js.map +1 -1
  73. package/dist/app-server/one-shot-json.d.ts +26 -0
  74. package/dist/app-server/one-shot-json.d.ts.map +1 -0
  75. package/dist/app-server/one-shot-json.js +168 -0
  76. package/dist/app-server/one-shot-json.js.map +1 -0
  77. package/dist/app-server/provider-models.d.ts +1 -6
  78. package/dist/app-server/provider-models.d.ts.map +1 -1
  79. package/dist/app-server/provider-models.js +11 -12
  80. package/dist/app-server/provider-models.js.map +1 -1
  81. package/dist/app-server/request-origin.d.ts +6 -0
  82. package/dist/app-server/request-origin.d.ts.map +1 -0
  83. package/dist/app-server/request-origin.js +61 -0
  84. package/dist/app-server/request-origin.js.map +1 -0
  85. package/dist/app-server/result-assistant-domain.d.ts +46 -48
  86. package/dist/app-server/result-assistant-domain.d.ts.map +1 -1
  87. package/dist/app-server/result-assistant-domain.js +148 -133
  88. package/dist/app-server/result-assistant-domain.js.map +1 -1
  89. package/dist/app-server/result-assistant.d.ts +2 -2
  90. package/dist/app-server/result-assistant.d.ts.map +1 -1
  91. package/dist/app-server/result-assistant.js +43 -311
  92. package/dist/app-server/result-assistant.js.map +1 -1
  93. package/dist/app-server/router.d.ts +0 -2
  94. package/dist/app-server/router.d.ts.map +1 -1
  95. package/dist/app-server/router.js +69 -71
  96. package/dist/app-server/router.js.map +1 -1
  97. package/dist/app-server/run-queue-executor.d.ts +2 -14
  98. package/dist/app-server/run-queue-executor.d.ts.map +1 -1
  99. package/dist/app-server/run-queue-executor.js +7 -16
  100. package/dist/app-server/run-queue-executor.js.map +1 -1
  101. package/dist/app-server/runs-routes.d.ts.map +1 -1
  102. package/dist/app-server/runs-routes.js +16 -15
  103. package/dist/app-server/runs-routes.js.map +1 -1
  104. package/dist/app-server/scenario-assistant-domain.d.ts +43 -136
  105. package/dist/app-server/scenario-assistant-domain.d.ts.map +1 -1
  106. package/dist/app-server/scenario-assistant-domain.js +377 -215
  107. package/dist/app-server/scenario-assistant-domain.js.map +1 -1
  108. package/dist/app-server/scenario-assistant.d.ts +3 -3
  109. package/dist/app-server/scenario-assistant.d.ts.map +1 -1
  110. package/dist/app-server/scenario-assistant.js +52 -406
  111. package/dist/app-server/scenario-assistant.js.map +1 -1
  112. package/dist/app-server/settings-store.d.ts +3 -7
  113. package/dist/app-server/settings-store.d.ts.map +1 -1
  114. package/dist/app-server/settings-store.js +35 -20
  115. package/dist/app-server/settings-store.js.map +1 -1
  116. package/dist/app-server/tool-analysis-domain.d.ts +1 -1
  117. package/dist/app-server/tool-analysis-domain.d.ts.map +1 -1
  118. package/dist/app-server/tool-analysis-domain.js +5 -4
  119. package/dist/app-server/tool-analysis-domain.js.map +1 -1
  120. package/dist/app-server/types.d.ts +3 -4
  121. package/dist/app-server/types.d.ts.map +1 -1
  122. package/dist/cli.js +6 -6
  123. package/dist/cli.js.map +1 -1
  124. package/package.json +10 -4
  125. package/dist/app/assets/index-256LN2cO.css +0 -2
  126. package/dist/app/assets/index-DVIaEnjQ.js +0 -243
@@ -1,37 +1,55 @@
1
- import { isLlmAgent } from '@inspectr/mcplab-core';
2
- import { chatWithJsonRetry, parseJsonFromAssistantText, cleanupSessionsByTtl, makeAssistantToolPublicName, newAssistantEntityId, throwIfAborted, touchSession, truncateJson, withTimeout } from './assistant-common.js';
1
+ import { McpClientManager } from '@inspectr/mcplab-core';
2
+ import { ASSISTANT_TOOL_TIMEOUT_MESSAGE, assistantToolDisplay, ASSISTANT_TOOL_TIMEOUT_MS, cleanupSessionsByTtl, makeAssistantToolPublicName, touchSession, truncateJson, withTimeout } from './assistant-common.js';
3
3
  import { endAssistantSseClients } from './assistant-events.js';
4
- export { truncateJson } from './assistant-common.js';
4
+ import { setupAssistantRuntimeSession } from './assistant-service.js';
5
+ import { createMcpContextTools, MCP_CONTEXT_TOOL_NAMES, mcpContextToolGuidance } from './assistant-mcp-context-tools.js';
6
+ import { APPROVAL_GUIDANCE } from './assistant-runtime-policy.js';
7
+ function createScenarioAssistantResources(agentConfig, context) {
8
+ return {
9
+ agentConfig,
10
+ mcp: new McpClientManager(),
11
+ tools: [],
12
+ toolPublicMap: new Map(),
13
+ contextServers: { resources: [], prompts: [] },
14
+ context
15
+ };
16
+ }
5
17
  const SCENARIO_ASSISTANT_SESSION_TTL_MS = 30 * 60 * 1000;
6
- const SCENARIO_ASSISTANT_MAX_TOOL_CALLS_PER_TURN = 3;
7
- const SCENARIO_ASSISTANT_TOOL_RESULT_PREVIEW_CHARS = 4000;
8
- export function cleanupAssistantSessions(sessions, now = Date.now()) {
9
- cleanupSessionsByTtl(sessions, SCENARIO_ASSISTANT_SESSION_TTL_MS, now, endAssistantSseClients);
18
+ // The hard TTL also aborts work in abandoned sessions; active operations refresh lastTouchedAt.
19
+ export function cleanupScenarioAssistantSessions(sessions, now = Date.now()) {
20
+ cleanupSessionsByTtl(sessions, SCENARIO_ASSISTANT_SESSION_TTL_MS, now, (session) => {
21
+ void Promise.resolve(session.runtime.dispose()).catch(() => { });
22
+ endAssistantSseClients(session);
23
+ });
10
24
  }
11
- export function touchAssistantSession(session) {
25
+ export function touchScenarioAssistantSession(session) {
12
26
  touchSession(session);
13
27
  }
14
- export function assistantSessionView(session) {
28
+ export function scenarioAssistantSessionView(session) {
29
+ const runtimeSnapshot = session.runtime.getSnapshot();
15
30
  return {
16
31
  id: session.id,
17
32
  createdAt: new Date(session.createdAt).toISOString(),
18
33
  updatedAt: new Date(session.lastTouchedAt).toISOString(),
19
34
  selectedAssistantAgentName: session.selectedAssistantAgentName,
20
- model: session.agentConfig.model,
21
- provider: session.agentConfig.provider,
35
+ model: session.model,
36
+ provider: session.provider,
22
37
  warnings: session.warnings,
23
- toolsLoaded: session.tools.length,
24
- toolServers: Array.from(new Set(session.tools.map((tool) => tool.name.split('__')[0]))),
25
- messages: session.chatMessages,
26
- pendingToolCalls: session.pendingToolCalls
38
+ toolsLoaded: session.toolsLoaded,
39
+ toolServers: session.toolServers,
40
+ revision: runtimeSnapshot.revision,
41
+ messages: runtimeSnapshot.messages,
42
+ pendingToolCalls: runtimeSnapshot.pendingToolCalls,
43
+ activeTurnId: runtimeSnapshot.activeTurnId,
44
+ streamingMessage: runtimeSnapshot.streamingMessage
27
45
  };
28
46
  }
29
- function assistantSystemPrompt(session) {
30
- if (session.systemPromptCache)
31
- return session.systemPromptCache;
32
- const { scenario } = session.context;
33
- const toolLines = session.tools.map((tool) => {
34
- const mapping = session.toolPublicMap.get(tool.name);
47
+ function assistantSystemPrompt(session, state) {
48
+ if (state.systemPromptCache)
49
+ return state.systemPromptCache;
50
+ const { scenario } = state.context;
51
+ const toolLines = state.tools.map((tool) => {
52
+ const mapping = state.toolPublicMap.get(tool.name);
35
53
  const schemaText = tool.inputSchema ? truncateJson(tool.inputSchema, 500) : '{}';
36
54
  return `- ${tool.name} (server=${mapping?.server ?? 'unknown'}, tool=${mapping?.tool ?? tool.name}) schema=${schemaText}`;
37
55
  });
@@ -39,18 +57,17 @@ function assistantSystemPrompt(session) {
39
57
  'You are a Scenario Authoring Assistant for MCP evaluation scenarios.',
40
58
  'Goal: help the user author deterministic scenario prompt, Checks (pass/fail), and Value Capture Rules.',
41
59
  'Use the available MCP tools and schemas to ground suggestions.',
42
- 'If you need live MCP information, call a tool and wait for approval.',
60
+ APPROVAL_GUIDANCE,
61
+ ...mcpContextToolGuidance(state.contextServers),
43
62
  'Tool selection policy: prefer search_* tools first for retrieval; fall back to list_* tools when the query is unknown, broad, or full coverage is required.',
44
63
  'Use user-facing terminology in your text responses: "Checks" and "Value Capture Rules" (not "eval rules" / "extract rules").',
45
- 'Respond ONLY as JSON with one of these envelopes:',
46
- `{"type":"assistant_message","text":"...","suggestions":{...optional...}}`,
47
- `{"type":"tool_call_request","text":"...","toolCall":{"name":"PUBLIC_TOOL_NAME","arguments":{}},"suggestions":{...optional...}}`,
48
- 'For suggestions, use keys: prompt, evalRules, extractRules, notes.',
64
+ 'Respond naturally in ordinary prose. Use the local propose_scenario_changes tool whenever you propose scenario edits.',
65
+ 'The local proposal tool accepts these keys: prompt, evalRules, extractRules, notes.',
49
66
  'prompt: { replacement: string, rationale?: string }',
50
67
  'evalRules: { replacement: [{ type, tool?, value?, sequence?, path?, equals?, label?, prompt? }...], rationale?: string }',
51
68
  'extractRules: { replacement: [{ name, pattern }...], rationale?: string }',
52
- 'If you propose any edits to the scenario (prompt, Checks, or Value Capture Rules), you MUST include the corresponding structured suggestions payload.',
53
- 'Do not describe "suggested updates" in text only. Include suggestions so the UI can render Apply actions.',
69
+ 'Use the proposal tool with a complete replacement for every field you propose to change, plus a rationale.',
70
+ 'Include only the fields you are changing; omit the others entirely.',
54
71
  'Keep rule types limited to: required_tool, forbidden_tool, tool_sequence, tool_input_contains, tool_input_regex, tool_input_jsonpath, response_contains, response_not_contains, response_starts_with, response_ends_with, response_equals, response_regex, response_jsonpath, response_jsonpath_exists, response_jsonpath_not_exists, agent_check.',
55
72
  'Optional checks you may suggest when useful: tool_sequence for ordered tool-call sequences and agent_check for semantic, fuzzy, or intent-based validation.',
56
73
  'Use tool_sequence when the order of tool calls matters and you want to validate that a sequence appears in the run in order, even if other tools happen between the listed tools.',
@@ -77,7 +94,7 @@ function assistantSystemPrompt(session) {
77
94
  ? `Available MCP tools:\n${toolLines.join('\n')}`
78
95
  : 'No MCP tools available.'
79
96
  ].join('\n');
80
- session.systemPromptCache = prompt;
97
+ state.systemPromptCache = prompt;
81
98
  return prompt;
82
99
  }
83
100
  function formatAssistantMcpPreloadError(serverName, error) {
@@ -93,33 +110,128 @@ function formatAssistantMcpPreloadError(serverName, error) {
93
110
  }
94
111
  return `Scenario Assistant MCP preload failed for server '${serverName}': ${raw}`;
95
112
  }
96
- export async function preloadAssistantTools(session, serversByName, selectedServerNames, options) {
97
- const usedNames = new Set();
98
- for (const serverName of selectedServerNames) {
99
- const server = serversByName[serverName];
100
- if (!server) {
101
- session.warnings.push(`Scenario Assistant: server '${serverName}' not found in config.`);
102
- continue;
103
- }
104
- try {
105
- await session.mcp.connectAll({ [serverName]: server }, undefined, {
106
- serverAuthHeaders: options?.serverAuthHeaders
107
- });
108
- const tools = await session.mcp.listTools(serverName);
109
- for (const tool of tools) {
110
- const publicName = makeAssistantToolPublicName(serverName, tool.name, usedNames);
111
- session.toolPublicMap.set(publicName, { server: serverName, tool: tool.name });
112
- session.tools.push({
113
- ...tool,
114
- name: publicName,
115
- description: `${tool.description ?? ''}\n[server=${serverName} tool=${tool.name}]`.trim()
116
- });
113
+ function createScenarioAssistantSetup(session, agentConfig, context) {
114
+ const state = createScenarioAssistantResources(agentConfig, context);
115
+ let disposePromise;
116
+ const dispose = () => {
117
+ disposePromise ??= Promise.resolve().then(() => state.mcp.disconnectAll());
118
+ return disposePromise;
119
+ };
120
+ return {
121
+ dispose,
122
+ async preload(serversByName, selectedServerNames, options) {
123
+ const usedNames = new Set();
124
+ for (const serverName of selectedServerNames) {
125
+ const server = serversByName[serverName];
126
+ if (!server) {
127
+ session.warnings.push(`Scenario Assistant: server '${serverName}' not found in config.`);
128
+ continue;
129
+ }
130
+ try {
131
+ await state.mcp.connectAll({ [serverName]: server }, undefined, {
132
+ serverAuthHeaders: options?.serverAuthHeaders
133
+ });
134
+ }
135
+ catch (error) {
136
+ session.warnings.push(formatAssistantMcpPreloadError(serverName, error));
137
+ continue;
138
+ }
139
+ // A server may offer only resources or prompts, so detect them independently of tools.
140
+ let features = { tools: true, resources: false, prompts: false };
141
+ try {
142
+ features = state.mcp.getServerFeatures(serverName);
143
+ }
144
+ catch {
145
+ // Treat the server as offering tools only.
146
+ }
147
+ if (features.resources)
148
+ state.contextServers.resources.push(serverName);
149
+ if (features.prompts)
150
+ state.contextServers.prompts.push(serverName);
151
+ if (!features.tools)
152
+ continue;
153
+ try {
154
+ const tools = await state.mcp.listTools(serverName);
155
+ for (const tool of tools) {
156
+ const publicName = makeAssistantToolPublicName(serverName, tool.name, usedNames);
157
+ state.toolPublicMap.set(publicName, {
158
+ server: serverName,
159
+ tool: tool.name,
160
+ display: assistantToolDisplay(tool)
161
+ });
162
+ state.tools.push({
163
+ ...tool,
164
+ name: publicName,
165
+ description: `${tool.description ?? ''}\n[server=${serverName} tool=${tool.name}]`.trim()
166
+ });
167
+ }
168
+ }
169
+ catch (error) {
170
+ session.warnings.push(formatAssistantMcpPreloadError(serverName, error));
171
+ }
117
172
  }
173
+ session.toolsLoaded = state.tools.length;
174
+ session.toolServers = Array.from(new Set(state.tools.map((tool) => tool.name.split('__')[0])));
175
+ },
176
+ createRuntimeDefinition() {
177
+ return {
178
+ assistant: 'scenario',
179
+ agent: state.agentConfig,
180
+ context: state.context,
181
+ initialMessages: session.initialMessages,
182
+ dispose,
183
+ getSystemPrompt: () => assistantSystemPrompt(session, state),
184
+ tools: [
185
+ ...state.tools.map((tool) => {
186
+ const mapping = state.toolPublicMap.get(tool.name);
187
+ return {
188
+ name: tool.name,
189
+ server: mapping.server,
190
+ tool: mapping.tool,
191
+ description: tool.description,
192
+ inputSchema: tool.inputSchema,
193
+ display: mapping.display,
194
+ execute: async (arguments_, signal) => withTimeout(() => state.mcp.callTool(mapping.server, mapping.tool, arguments_, { signal }), ASSISTANT_TOOL_TIMEOUT_MS, ASSISTANT_TOOL_TIMEOUT_MESSAGE, signal)
195
+ };
196
+ }),
197
+ {
198
+ name: PROPOSE_SCENARIO_CHANGES,
199
+ server: 'local',
200
+ tool: PROPOSE_SCENARIO_CHANGES,
201
+ description: 'Submit structured scenario changes for the user to review and apply.',
202
+ display: { title: 'Propose scenario changes' },
203
+ inputSchema: proposalSchema,
204
+ execute: async (args, _signal) => validateScenarioAssistantProposal(args, state.toolPublicMap),
205
+ extractProposal: (result) => result
206
+ },
207
+ ...createMcpContextTools(state.mcp, state.contextServers)
208
+ ],
209
+ approvalPolicy: (name) => name === PROPOSE_SCENARIO_CHANGES
210
+ ? 'local'
211
+ : MCP_CONTEXT_TOOL_NAMES.includes(name) ||
212
+ state.tools.find((tool) => tool.name === name)?.annotations?.readOnlyHint === true
213
+ ? 'automatic'
214
+ : 'approval_required'
215
+ };
118
216
  }
119
- catch (error) {
120
- session.warnings.push(formatAssistantMcpPreloadError(serverName, error));
121
- }
122
- }
217
+ };
218
+ }
219
+ export async function setupScenarioAssistantSession(params) {
220
+ const setup = createScenarioAssistantSetup(params.session, params.agentConfig, params.context);
221
+ return setupAssistantRuntimeSession({
222
+ session: params.session,
223
+ factory: params.assistantRuntimeFactory,
224
+ disposeSetup: setup.dispose,
225
+ prepare: async () => {
226
+ await setup.preload(params.serversByName, params.selectedServerNames, {
227
+ serverAuthHeaders: params.serverAuthHeaders
228
+ });
229
+ return setup.createRuntimeDefinition();
230
+ },
231
+ initialEvents: params.initialEvents,
232
+ onEvent: params.onEvent,
233
+ onActivity: params.onActivity
234
+ });
123
235
  }
124
236
  function normalizeEvalRuleToolNames(suggestions, toolPublicMap) {
125
237
  if (!suggestions?.evalRules?.replacement)
@@ -414,176 +526,226 @@ export function normalizeScenarioAssistantEvalRules(replacement) {
414
526
  }
415
527
  return lintContradictoryEvalRules(intentDeduplicateEvalRules(collapseOffByOneCountGuards(deduped)));
416
528
  }
417
- function normalizeEvalRuleSuggestions(suggestions) {
418
- if (!suggestions?.evalRules?.replacement)
419
- return;
420
- suggestions.evalRules.replacement = normalizeScenarioAssistantEvalRules(suggestions.evalRules.replacement);
421
- }
422
- function parseAssistantModelOutput(text) {
423
- const parsed = parseJsonFromAssistantText(text);
424
- if (!parsed || typeof parsed !== 'object') {
425
- throw new Error('Assistant response must be a JSON object');
426
- }
427
- const parsedObj = parsed;
428
- if (parsedObj.type !== 'assistant_message' && parsedObj.type !== 'tool_call_request') {
429
- const receivedType = typeof parsedObj.type === 'string' ? `"${parsedObj.type}"` : '(missing)';
430
- throw new Error(`Assistant response type ${receivedType} is unsupported; expected 'assistant_message' or 'tool_call_request'`);
431
- }
432
- if (typeof parsedObj.text !== 'string') {
433
- throw new Error('Assistant response missing text');
434
- }
435
- if (parsedObj.type === 'tool_call_request') {
436
- if (!parsedObj.toolCall || typeof parsedObj.toolCall !== 'object') {
437
- throw new Error('Assistant tool_call_request missing toolCall');
438
- }
439
- if (typeof parsedObj.toolCall.name !== 'string' || !parsedObj.toolCall.name.trim()) {
440
- throw new Error('Assistant toolCall.name must be a non-empty string');
529
+ const PROPOSE_SCENARIO_CHANGES = 'propose_scenario_changes';
530
+ const ruleTypes = new Set([
531
+ 'required_tool',
532
+ 'forbidden_tool',
533
+ 'tool_sequence',
534
+ 'tool_input_contains',
535
+ 'tool_input_regex',
536
+ 'tool_input_jsonpath',
537
+ 'response_contains',
538
+ 'response_not_contains',
539
+ 'response_starts_with',
540
+ 'response_ends_with',
541
+ 'response_equals',
542
+ 'response_regex',
543
+ 'response_jsonpath',
544
+ 'response_jsonpath_exists',
545
+ 'response_jsonpath_not_exists',
546
+ 'agent_check'
547
+ ]);
548
+ const proposalSchema = {
549
+ type: 'object',
550
+ additionalProperties: false,
551
+ minProperties: 1,
552
+ properties: {
553
+ prompt: {
554
+ type: 'object',
555
+ description: 'Omit this field if you are not changing it; do not send null or empty values.',
556
+ additionalProperties: false,
557
+ required: ['replacement'],
558
+ properties: { replacement: { type: 'string' }, rationale: { type: 'string' } }
559
+ },
560
+ evalRules: {
561
+ type: 'object',
562
+ description: 'Omit this field if you are not changing it; do not send null or empty values. Use replacement: [] to remove all checks.',
563
+ additionalProperties: false,
564
+ required: ['replacement'],
565
+ properties: {
566
+ replacement: {
567
+ type: 'array',
568
+ items: {
569
+ type: 'object',
570
+ additionalProperties: false,
571
+ required: ['type'],
572
+ properties: {
573
+ type: { type: 'string', enum: [...ruleTypes] },
574
+ tool: { type: 'string' },
575
+ value: { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }] },
576
+ sequence: { type: 'array', items: { type: 'string' } },
577
+ path: { type: 'string' },
578
+ equals: { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }] },
579
+ label: { type: 'string' },
580
+ prompt: { type: 'string' }
581
+ }
582
+ }
583
+ },
584
+ rationale: { type: 'string' }
585
+ }
586
+ },
587
+ extractRules: {
588
+ type: 'object',
589
+ description: 'Omit this field if you are not changing it; do not send null or empty values. Use replacement: [] to remove all capture rules.',
590
+ additionalProperties: false,
591
+ required: ['replacement'],
592
+ properties: {
593
+ replacement: {
594
+ type: 'array',
595
+ items: {
596
+ type: 'object',
597
+ additionalProperties: false,
598
+ required: ['name', 'pattern'],
599
+ properties: { name: { type: 'string' }, pattern: { type: 'string' } }
600
+ }
601
+ },
602
+ rationale: { type: 'string' }
603
+ }
604
+ },
605
+ notes: {
606
+ type: 'array',
607
+ description: 'Optional notes accompanying changes. Omit if unused; notes alone are not a change.',
608
+ items: { type: 'string' }
441
609
  }
442
610
  }
443
- return parsedObj;
444
- }
445
- function safeAssistantTextFromResponse(rawText) {
446
- try {
447
- return parseAssistantModelOutput(rawText).text;
448
- }
449
- catch {
450
- return rawText.trim();
451
- }
452
- }
453
- async function assistantChatModel(session, signal) {
454
- return chatWithJsonRetry({
455
- agent: session.agentConfig,
456
- messages: session.llmMessages,
457
- tools: session.tools,
458
- system: assistantSystemPrompt(session),
459
- parse: parseAssistantModelOutput,
460
- toolCallFallbackText: (toolName) => `I need to call '${toolName}' to help answer.`,
461
- signal
462
- });
463
- }
464
- export async function continueAssistantTurn(session, signal) {
465
- const pendingCountForTurn = session.pendingToolCalls.filter((c) => c.status === 'pending').length;
466
- if (pendingCountForTurn > SCENARIO_ASSISTANT_MAX_TOOL_CALLS_PER_TURN) {
467
- throw new Error('Scenario Assistant exceeded maximum pending tool calls for this turn');
611
+ };
612
+ export function validateScenarioAssistantProposal(input, toolPublicMap) {
613
+ const invalid = (path, issue) => {
614
+ throw new Error(`Invalid scenario proposal at ${path}: ${issue}`);
615
+ };
616
+ if (!input || typeof input !== 'object' || Array.isArray(input))
617
+ return invalid('proposal', 'expected an object');
618
+ const isUnchangedPlaceholder = (field) => field === null ||
619
+ field === '' ||
620
+ (typeof field === 'object' && !Array.isArray(field) && Object.keys(field).length === 0);
621
+ const value = Object.fromEntries(Object.entries(input).filter(([, field]) => !isUnchangedPlaceholder(field)));
622
+ const checkKeys = (object, allowed, path) => {
623
+ const unknown = Object.keys(object).find((key) => !allowed.includes(key));
624
+ if (unknown !== undefined)
625
+ invalid(`${path}.${unknown}`, 'unknown key');
626
+ };
627
+ checkKeys(value, ['prompt', 'evalRules', 'extractRules', 'notes'], 'proposal');
628
+ const result = {};
629
+ const validRationale = (obj) => obj.rationale === undefined || typeof obj.rationale === 'string';
630
+ if (value.prompt !== undefined) {
631
+ if (!value.prompt || typeof value.prompt !== 'object' || Array.isArray(value.prompt))
632
+ return invalid('prompt', 'expected an object');
633
+ const p = value.prompt;
634
+ checkKeys(p, ['replacement', 'rationale'], 'prompt');
635
+ if (typeof p.replacement !== 'string' || !p.replacement.trim())
636
+ return invalid('prompt.replacement', 'expected a non-empty string');
637
+ if (!validRationale(p))
638
+ return invalid('prompt.rationale', 'expected a string');
639
+ result.prompt = {
640
+ replacement: p.replacement,
641
+ ...(p.rationale === undefined ? {} : { rationale: p.rationale })
642
+ };
468
643
  }
469
- const modelOutput = await assistantChatModel(session, signal);
470
- throwIfAborted(signal);
471
- normalizeEvalRuleToolNames(modelOutput.suggestions, session.toolPublicMap);
472
- normalizeEvalRuleSuggestions(modelOutput.suggestions);
473
- if (modelOutput.type === 'tool_call_request') {
474
- const requestedCalls = 'toolCalls' in modelOutput && Array.isArray(modelOutput.toolCalls)
475
- ? modelOutput.toolCalls
476
- : [modelOutput.toolCall];
477
- const pendingCalls = [];
478
- const llmToolCalls = [];
479
- for (const requested of requestedCalls) {
480
- const mapping = session.toolPublicMap.get(requested.name);
481
- if (!mapping) {
482
- throw new Error(`Scenario Assistant requested unknown tool '${requested.name}'. Available tools: ${session.tools.map((t) => t.name).join(', ')}`);
644
+ if (value.evalRules !== undefined) {
645
+ if (!value.evalRules || typeof value.evalRules !== 'object' || Array.isArray(value.evalRules))
646
+ return invalid('evalRules', 'expected an object');
647
+ const e = value.evalRules;
648
+ checkKeys(e, ['replacement', 'rationale'], 'evalRules');
649
+ if (!Array.isArray(e.replacement))
650
+ return invalid('evalRules.replacement', 'expected an array');
651
+ if (!validRationale(e))
652
+ return invalid('evalRules.rationale', 'expected a string');
653
+ for (const [index, rule] of e.replacement.entries()) {
654
+ const path = `evalRules.replacement[${index}]`;
655
+ if (!rule || typeof rule !== 'object' || Array.isArray(rule))
656
+ return invalid(path, 'expected an object');
657
+ const r = rule;
658
+ checkKeys(r, ['type', 'tool', 'value', 'sequence', 'path', 'equals', 'label', 'prompt'], path);
659
+ if (typeof r.type !== 'string')
660
+ return invalid(`${path}.type`, 'expected a rule type');
661
+ if (!ruleTypes.has(r.type))
662
+ return invalid(`${path}.type`, `unsupported rule type '${r.type}'`);
663
+ for (const key of ['tool', 'path', 'label', 'prompt'])
664
+ if (r[key] !== undefined && typeof r[key] !== 'string')
665
+ return invalid(`${path}.${key}`, 'expected a string');
666
+ if (r.value !== undefined && !['string', 'number', 'boolean'].includes(typeof r.value))
667
+ return invalid(`${path}.value`, 'expected a string, number, or boolean');
668
+ if (r.equals !== undefined && !['string', 'number', 'boolean'].includes(typeof r.equals))
669
+ return invalid(`${path}.equals`, 'expected a string, number, or boolean');
670
+ if (r.sequence !== undefined &&
671
+ (!Array.isArray(r.sequence) || !r.sequence.every((x) => typeof x === 'string')))
672
+ return invalid(`${path}.sequence`, 'expected an array of strings');
673
+ const type = r.type;
674
+ if (type === 'required_tool' ||
675
+ type === 'forbidden_tool' ||
676
+ (type.startsWith('response_') && !type.includes('jsonpath'))) {
677
+ if (typeof r.value !== 'string')
678
+ return invalid(`${path}.value`, 'required and must be a string');
483
679
  }
484
- const pending = {
485
- id: newAssistantEntityId('satc'),
486
- server: mapping.server,
487
- tool: mapping.tool,
488
- publicToolName: requested.name,
489
- arguments: requested.arguments ?? {},
490
- status: 'pending',
491
- createdAt: new Date().toISOString()
492
- };
493
- throwIfAborted(signal);
494
- session.pendingToolCalls.push(pending);
495
- pendingCalls.push(pending);
496
- llmToolCalls.push({
497
- id: pending.id,
498
- name: pending.publicToolName,
499
- arguments: pending.arguments
500
- });
501
- }
502
- const firstPending = pendingCalls[0];
503
- session.chatMessages.push({
504
- id: newAssistantEntityId('msg'),
505
- role: 'assistant',
506
- text: modelOutput.text,
507
- createdAt: new Date().toISOString(),
508
- suggestions: modelOutput.suggestions,
509
- pendingToolCallId: firstPending.id,
510
- pendingToolCallIds: pendingCalls.map((p) => p.id),
511
- toolRequestServer: firstPending.server,
512
- toolRequestName: firstPending.tool,
513
- toolRequestPublicName: firstPending.publicToolName
514
- });
515
- session.llmMessages.push({
516
- role: 'assistant',
517
- content: modelOutput.text,
518
- tool_calls: llmToolCalls
519
- });
520
- touchAssistantSession(session);
521
- return {
522
- session: assistantSessionView(session),
523
- response: {
524
- type: 'tool_call_request',
525
- text: modelOutput.text,
526
- suggestions: modelOutput.suggestions,
527
- pendingToolCall: firstPending,
528
- pendingToolCalls: pendingCalls
680
+ if (type === 'tool_sequence' && (!Array.isArray(r.sequence) || r.sequence.length === 0))
681
+ return invalid(`${path}.sequence`, 'required and must be a non-empty array');
682
+ // Blank names are dropped when the rule is normalized, which could empty the sequence.
683
+ if (type === 'tool_sequence' && r.sequence.some((toolName) => !toolName.trim()))
684
+ return invalid(`${path}.sequence`, 'tool names must not be blank');
685
+ if (type.startsWith('tool_input_') && typeof r.tool !== 'string')
686
+ return invalid(`${path}.tool`, 'required and must be a string');
687
+ if ((type === 'tool_input_contains' || type === 'tool_input_regex') &&
688
+ typeof r.value !== 'string')
689
+ return invalid(`${path}.value`, 'required and must be a string');
690
+ if ((type === 'tool_input_regex' || type === 'response_regex') && typeof r.value !== 'string')
691
+ return invalid(`${path}.value`, 'required and must be a string');
692
+ if (type.endsWith('jsonpath') ||
693
+ type.endsWith('jsonpath_exists') ||
694
+ type.endsWith('jsonpath_not_exists')) {
695
+ if (typeof r.path !== 'string')
696
+ return invalid(`${path}.path`, 'required and must be a string');
529
697
  }
698
+ if (type === 'agent_check' && typeof r.label !== 'string')
699
+ return invalid(`${path}.label`, 'required and must be a string');
700
+ if (type === 'agent_check' && typeof r.prompt !== 'string')
701
+ return invalid(`${path}.prompt`, 'required and must be a string');
702
+ }
703
+ const rules = structuredClone(e.replacement);
704
+ const bundle = { evalRules: { replacement: rules } };
705
+ normalizeEvalRuleToolNames(bundle, toolPublicMap);
706
+ bundle.evalRules.replacement = normalizeScenarioAssistantEvalRules(bundle.evalRules.replacement);
707
+ result.evalRules = {
708
+ ...bundle.evalRules,
709
+ ...(e.rationale === undefined ? {} : { rationale: e.rationale })
530
710
  };
531
711
  }
532
- session.chatMessages.push({
533
- id: newAssistantEntityId('msg'),
534
- role: 'assistant',
535
- text: modelOutput.text,
536
- createdAt: new Date().toISOString(),
537
- suggestions: modelOutput.suggestions
538
- });
539
- session.llmMessages.push({
540
- role: 'assistant',
541
- content: JSON.stringify(modelOutput)
542
- });
543
- touchAssistantSession(session);
544
- return {
545
- session: assistantSessionView(session),
546
- response: {
547
- type: 'assistant_message',
548
- text: modelOutput.text,
549
- suggestions: modelOutput.suggestions
712
+ if (value.extractRules !== undefined) {
713
+ if (!value.extractRules ||
714
+ typeof value.extractRules !== 'object' ||
715
+ Array.isArray(value.extractRules))
716
+ return invalid('extractRules', 'expected an object');
717
+ const e = value.extractRules;
718
+ checkKeys(e, ['replacement', 'rationale'], 'extractRules');
719
+ if (!Array.isArray(e.replacement))
720
+ return invalid('extractRules.replacement', 'expected an array');
721
+ if (!validRationale(e))
722
+ return invalid('extractRules.rationale', 'expected a string');
723
+ for (const [index, item] of e.replacement.entries()) {
724
+ const path = `extractRules.replacement[${index}]`;
725
+ if (!item || typeof item !== 'object' || Array.isArray(item))
726
+ return invalid(path, 'expected an object');
727
+ const rule = item;
728
+ checkKeys(rule, ['name', 'pattern'], path);
729
+ if (typeof rule.name !== 'string')
730
+ return invalid(`${path}.name`, 'required and must be a string');
731
+ if (typeof rule.pattern !== 'string')
732
+ return invalid(`${path}.pattern`, 'required and must be a string');
550
733
  }
551
- };
552
- }
553
- export async function executeAssistantToolCall(session, pending, signal) {
554
- const timeoutMs = 30_000;
555
- return withTimeout(() => session.mcp.callTool(pending.server, pending.tool, pending.arguments, {
556
- signal
557
- }), timeoutMs, `Tool call timed out after ${timeoutMs}ms`, signal);
558
- }
559
- export function summarizeToolResultForAssistant(result) {
560
- return truncateJson(result, SCENARIO_ASSISTANT_TOOL_RESULT_PREVIEW_CHARS);
561
- }
562
- export function resolveAssistantAgentFromConfig(config, selectedAssistantAgentName) {
563
- const agent = config.agents[selectedAssistantAgentName];
564
- if (!agent) {
565
- throw new Error(`Scenario Assistant agent '${selectedAssistantAgentName}' not found in resolved config agents.`);
734
+ result.extractRules = {
735
+ replacement: structuredClone(e.replacement),
736
+ ...(e.rationale === undefined ? {} : { rationale: e.rationale })
737
+ };
566
738
  }
567
- if (!isLlmAgent(agent))
568
- throw new Error(`Browser Agent '${selectedAssistantAgentName}' cannot be used by Scenario Assistant.`);
569
- return agent;
570
- }
571
- export function resolveAssistantAgentFromLibraries(libraries, selectedAssistantAgentName) {
572
- const agent = libraries.agents[selectedAssistantAgentName];
573
- if (!agent) {
574
- throw new Error(`Scenario Assistant agent '${selectedAssistantAgentName}' not found in library agents. Configure the central Scenario Assistant Agent in Libraries > Scenarios.`);
739
+ if (value.notes !== undefined) {
740
+ if (!Array.isArray(value.notes))
741
+ return invalid('notes', 'expected an array');
742
+ for (const [index, note] of value.notes.entries())
743
+ if (typeof note !== 'string')
744
+ return invalid(`notes[${index}]`, 'expected a string');
745
+ result.notes = [...value.notes];
575
746
  }
576
- if (!isLlmAgent(agent))
577
- throw new Error(`Browser Agent '${selectedAssistantAgentName}' cannot be used by Scenario Assistant.`);
578
- return agent;
579
- }
580
- export function pickDefaultAssistantAgentName(params) {
581
- const requested = params.requested?.trim();
582
- if (requested)
583
- return requested;
584
- const settingsDefault = params.settingsDefault?.trim();
585
- if (settingsDefault)
586
- return settingsDefault;
587
- return params.agentNames[0] ?? '';
747
+ if (!result.prompt && !result.evalRules && !result.extractRules)
748
+ return invalid('proposal', 'must include at least one change');
749
+ return result;
588
750
  }
589
751
  //# sourceMappingURL=scenario-assistant-domain.js.map