@emseepea/testing 0.17.1 → 0.19.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 +36 -1
- package/package.json +1 -1
- package/semantic/cli.mjs +11 -0
- package/semantic/elicitation.mjs +51 -0
- package/semantic/provider.mjs +27 -8
- package/semantic/test.d.mts +21 -2
- package/semantic/test.mjs +36 -7
package/README.md
CHANGED
|
@@ -22,6 +22,13 @@ it with `assertToolArguments` for the stable call and `assertFeedback` for the
|
|
|
22
22
|
feedback observation and important detail. Keep `assertToolCalls` when every
|
|
23
23
|
complete argument should match exactly and no feedback tool is advertised.
|
|
24
24
|
|
|
25
|
+
For an intentionally optional permission check, use
|
|
26
|
+
`assertToolNames(turn, { oneOf: [["update"], ["permissions", "update"]] })`.
|
|
27
|
+
Supply one to eight explicit sequences. Each trial must match a complete
|
|
28
|
+
sequence in the listed order and count. The evidence retains all alternatives
|
|
29
|
+
and the matched sequence; arguments, meaning, confirmation and feedback still
|
|
30
|
+
need their own assertions.
|
|
31
|
+
|
|
25
32
|
For a successful turn that advertises feedback, use
|
|
26
33
|
`assertToolCallsWithOptionalFeedback`. It requires the exact ordered primary
|
|
27
34
|
calls and arguments, then allows no feedback call or one trailing
|
|
@@ -60,6 +67,33 @@ never production. Resources and prompts need deterministic protocol tests;
|
|
|
60
67
|
this library does not pretend that manually injecting their content proves a
|
|
61
68
|
native user journey.
|
|
62
69
|
|
|
70
|
+
## Scripted Confirmation Input
|
|
71
|
+
|
|
72
|
+
For tools that request form elicitation, supply an explicit simulated human
|
|
73
|
+
response for that turn. The script answers the real native client's request;
|
|
74
|
+
it is not sent to the model and does not replace a tool result.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const turn = await conversation.send("Create a contact named Synthetic Customer.", {
|
|
78
|
+
elicitations: [{
|
|
79
|
+
messageIncludes: ["Create", "Synthetic Customer"],
|
|
80
|
+
response: { action: "accept", content: { decision: "create" } },
|
|
81
|
+
}],
|
|
82
|
+
});
|
|
83
|
+
assertNoNegativeFeedback(turn);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Choose fragments that identify the exact proposed effect and recipient where
|
|
87
|
+
applicable. Every fragment must match one actual form request. Unexpected,
|
|
88
|
+
ambiguous, duplicate, and unused scripted responses fail the turn. No request
|
|
89
|
+
is approved by default; URL elicitation is unsupported.
|
|
90
|
+
|
|
91
|
+
Each of the three trials records the form message, schema, and scripted human
|
|
92
|
+
decision in `turn.elicitations` and saved evidence. This proves the tested
|
|
93
|
+
native client exchange with simulated human input, not compatibility with a
|
|
94
|
+
different client or a person's actual consent. Continue to use isolated,
|
|
95
|
+
synthetic servers; never point these tests at production.
|
|
96
|
+
|
|
63
97
|
## Test Extension Composition
|
|
64
98
|
|
|
65
99
|
Use `startEmseepea` in ordinary tests when you have an app factory and want to
|
|
@@ -381,6 +415,7 @@ tool arguments, tool results, assertions, or application context.
|
|
|
381
415
|
|
|
382
416
|
Provider events, MCP addresses, configuration, headers, provider and harness
|
|
383
417
|
credentials, environment values, stderr, and home-directory paths are not
|
|
384
|
-
retained.
|
|
418
|
+
retained. Known configured authentication values are scrubbed from elicitation
|
|
419
|
+
evidence. Arbitrary secrets placed inside test content are not detected.
|
|
385
420
|
Local evidence is written with mode `0600`; repository CI retains it for 14
|
|
386
421
|
days.
|
package/package.json
CHANGED
package/semantic/cli.mjs
CHANGED
|
@@ -172,6 +172,15 @@ function validToolAssertions(turn, isHash) {
|
|
|
172
172
|
|| (turn.toolCalls.length === 1 && turn.toolCalls[0].name === turn.expectedOptionalTool));
|
|
173
173
|
}
|
|
174
174
|
if (JSON.stringify(turn.selectedTools) !== JSON.stringify(turn.expectedTools)) return false;
|
|
175
|
+
if (turn.expectedToolAlternatives !== undefined) {
|
|
176
|
+
const alternatives = turn.expectedToolAlternatives;
|
|
177
|
+
if (!Array.isArray(alternatives) || alternatives.length < 1 || alternatives.length > 8
|
|
178
|
+
|| !alternatives.every((names) => Array.isArray(names)
|
|
179
|
+
&& names.every((name) => typeof name === "string" && name.trim()))
|
|
180
|
+
|| !alternatives.some((names) => JSON.stringify(names) === JSON.stringify(turn.expectedTools))) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
175
184
|
if (isHash(turn.expectedCallsSha256)) {
|
|
176
185
|
return JSON.stringify(turn.toolCalls.map(({ name, arguments: args }) => ({ name, arguments: args })))
|
|
177
186
|
=== JSON.stringify(turn.expectedCalls);
|
|
@@ -181,6 +190,8 @@ function validToolAssertions(turn, isHash) {
|
|
|
181
190
|
tools: turn.expectedTools,
|
|
182
191
|
arguments: turn.expectedArguments,
|
|
183
192
|
feedback: turn.expectedFeedback,
|
|
193
|
+
...(turn.expectedToolAlternatives === undefined
|
|
194
|
+
? {} : { alternatives: turn.expectedToolAlternatives }),
|
|
184
195
|
})).digest("hex");
|
|
185
196
|
if (expectedHash !== turn.expectedSelectionSha256) return false;
|
|
186
197
|
for (const [name, expected] of Object.entries(turn.expectedArguments ?? {})) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Scripted human input through the native client's control channel, never model guidance.
|
|
2
|
+
export function redactElicitationEvidence(value, secrets) {
|
|
3
|
+
let json = JSON.stringify(value);
|
|
4
|
+
for (const secret of secrets.filter((item) => typeof item === "string" && item)) {
|
|
5
|
+
// Escape exactly as JSON does, so embedded quotes and backslashes are scrubbed too.
|
|
6
|
+
const escaped = JSON.stringify(secret).slice(1, -1);
|
|
7
|
+
json = json.split(escaped).join("[REDACTED]");
|
|
8
|
+
}
|
|
9
|
+
return JSON.parse(json);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function scriptedElicitations(options = {}, secrets = []) {
|
|
13
|
+
const expected = options.elicitations ?? [];
|
|
14
|
+
if (!Array.isArray(expected) || expected.length > 8) throw new Error("Invalid scripted elicitations");
|
|
15
|
+
for (const entry of expected) {
|
|
16
|
+
if (!entry || !Array.isArray(entry.messageIncludes) || entry.messageIncludes.length === 0
|
|
17
|
+
|| entry.messageIncludes.length > 8
|
|
18
|
+
|| entry.messageIncludes.some((text) => typeof text !== "string" || !text || text.length > 4096)
|
|
19
|
+
|| !entry.response || !["accept", "decline", "cancel"].includes(entry.response.action)
|
|
20
|
+
|| JSON.stringify(entry.response).length > 16_384) throw new Error("Invalid scripted elicitation");
|
|
21
|
+
}
|
|
22
|
+
const fixtures = structuredClone(expected);
|
|
23
|
+
const used = new Set();
|
|
24
|
+
const evidence = [];
|
|
25
|
+
return {
|
|
26
|
+
evidence,
|
|
27
|
+
respond(request) {
|
|
28
|
+
if (request?.subtype !== "elicitation" || request.mcp_server_name !== "emseepea_eval"
|
|
29
|
+
|| (request.mode !== undefined && request.mode !== "form")
|
|
30
|
+
|| typeof request.message !== "string" || request.message.length > 16_384
|
|
31
|
+
|| !request.requested_schema || typeof request.requested_schema !== "object"
|
|
32
|
+
|| JSON.stringify(request.requested_schema).length > 16_384) {
|
|
33
|
+
throw new Error("Unsupported native elicitation request");
|
|
34
|
+
}
|
|
35
|
+
const item = redactElicitationEvidence({ mode: "form", message: request.message,
|
|
36
|
+
requestedSchema: request.requested_schema }, secrets);
|
|
37
|
+
evidence.push(item);
|
|
38
|
+
const matches = fixtures.map((entry, index) => ({ entry, index }))
|
|
39
|
+
.filter(({ entry }) => entry.messageIncludes.every((text) => request.message.includes(text)));
|
|
40
|
+
if (matches.length !== 1 || used.has(matches[0].index)) throw new Error("Unexpected or duplicate native elicitation");
|
|
41
|
+
const { entry, index } = matches[0];
|
|
42
|
+
used.add(index);
|
|
43
|
+
item.response = redactElicitationEvidence(entry.response, secrets);
|
|
44
|
+
item.inputSource = "scripted-human";
|
|
45
|
+
return entry.response;
|
|
46
|
+
},
|
|
47
|
+
finish() {
|
|
48
|
+
if (used.size !== fixtures.length) throw new Error("Unused scripted elicitation response");
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/semantic/provider.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { scriptedElicitations } from "./elicitation.mjs";
|
|
4
5
|
|
|
5
6
|
const model = "claude-sonnet-4-6";
|
|
6
7
|
const mcpServerName = "emseepea_eval";
|
|
@@ -236,7 +237,7 @@ export function conversationInvocation(provider, directory, url, tools, authToke
|
|
|
236
237
|
};
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
export function startModelConversation(provider, directory, url, tools, authToken, context, signal) {
|
|
240
|
+
export function startModelConversation(provider, directory, url, tools, authToken, context, signal, serverSecrets = []) {
|
|
240
241
|
signal?.throwIfAborted();
|
|
241
242
|
const invocation = conversationInvocation(provider, directory, url, tools, authToken, context);
|
|
242
243
|
const child = spawn(invocation.command, invocation.args, {
|
|
@@ -250,12 +251,13 @@ export function startModelConversation(provider, directory, url, tools, authToke
|
|
|
250
251
|
let initialEvents = [];
|
|
251
252
|
let initialized = false;
|
|
252
253
|
let closed = false;
|
|
254
|
+
const controlIds = new Set();
|
|
253
255
|
const fail = (message) => {
|
|
254
256
|
if (!pending) return;
|
|
255
257
|
clearTimeout(pending.timer);
|
|
256
|
-
const reject = pending
|
|
258
|
+
const { reject, script } = pending;
|
|
257
259
|
pending = undefined;
|
|
258
|
-
reject(new Error(message));
|
|
260
|
+
reject(Object.assign(new Error(message), { elicitations: script.evidence }));
|
|
259
261
|
};
|
|
260
262
|
const abort = () => { fail("Model conversation was cancelled"); child.kill("SIGKILL"); };
|
|
261
263
|
signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -277,6 +279,21 @@ export function startModelConversation(provider, directory, url, tools, authToke
|
|
|
277
279
|
child.kill("SIGKILL");
|
|
278
280
|
return;
|
|
279
281
|
}
|
|
282
|
+
if (event.type === "control_request") {
|
|
283
|
+
try {
|
|
284
|
+
if (!pending || typeof event.request_id !== "string" || !event.request_id
|
|
285
|
+
|| controlIds.has(event.request_id)) throw new Error("Unexpected native control request");
|
|
286
|
+
controlIds.add(event.request_id);
|
|
287
|
+
const response = pending.script.respond(event.request);
|
|
288
|
+
child.stdin.write(`${JSON.stringify({ type: "control_response", response: {
|
|
289
|
+
subtype: "success", request_id: event.request_id, response,
|
|
290
|
+
} })}\n`);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
fail(error.message);
|
|
293
|
+
child.kill("SIGKILL");
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
280
297
|
if (!pending) {
|
|
281
298
|
initialEvents.push(event);
|
|
282
299
|
continue;
|
|
@@ -284,13 +301,14 @@ export function startModelConversation(provider, directory, url, tools, authToke
|
|
|
284
301
|
pending.events.push(event);
|
|
285
302
|
if (event.type !== "result") continue;
|
|
286
303
|
clearTimeout(pending.timer);
|
|
287
|
-
const { events, resolve, reject } = pending;
|
|
304
|
+
const { events, resolve, reject, script } = pending;
|
|
288
305
|
pending = undefined;
|
|
289
306
|
try {
|
|
307
|
+
script.finish();
|
|
290
308
|
const turn = parseNativeClaudeEvents(events, tools, !initialized);
|
|
291
309
|
initialized = true;
|
|
292
|
-
resolve(turn);
|
|
293
|
-
} catch (error) { reject(error); }
|
|
310
|
+
resolve({ ...turn, elicitations: script.evidence });
|
|
311
|
+
} catch (error) { reject(Object.assign(error, { elicitations: script.evidence })); }
|
|
294
312
|
}
|
|
295
313
|
});
|
|
296
314
|
child.once("error", () => fail("Model conversation could not start"));
|
|
@@ -299,16 +317,17 @@ export function startModelConversation(provider, directory, url, tools, authToke
|
|
|
299
317
|
if (pending) fail(`Model conversation exited ${String(code)}`);
|
|
300
318
|
});
|
|
301
319
|
return Object.freeze({
|
|
302
|
-
send(prompt) {
|
|
320
|
+
send(prompt, options) {
|
|
303
321
|
if (closed || child.stdin.destroyed) throw new Error("Model conversation is closed");
|
|
304
322
|
if (pending) throw new Error("Model conversation already has a pending turn");
|
|
323
|
+
const script = scriptedElicitations(options, [authToken, invocation.env.CLAUDE_CODE_OAUTH_TOKEN, ...serverSecrets]);
|
|
305
324
|
return new Promise((resolve, reject) => {
|
|
306
325
|
const timer = setTimeout(() => {
|
|
307
326
|
fail("Model command timed out");
|
|
308
327
|
child.kill("SIGKILL");
|
|
309
328
|
}, 180_000);
|
|
310
329
|
timer.unref();
|
|
311
|
-
pending = { events: initialEvents, reject, resolve, timer };
|
|
330
|
+
pending = { events: initialEvents, reject, resolve, timer, script };
|
|
312
331
|
initialEvents = [];
|
|
313
332
|
child.stdin.write(`${JSON.stringify({
|
|
314
333
|
type: "user",
|
package/semantic/test.d.mts
CHANGED
|
@@ -18,11 +18,26 @@ export interface ConversationOptions {
|
|
|
18
18
|
export interface ConversationTurn {
|
|
19
19
|
readonly responses: readonly string[];
|
|
20
20
|
readonly toolCalls: readonly (readonly ToolCall[])[];
|
|
21
|
+
readonly elicitations: readonly (readonly ElicitationEvidence[])[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ScriptedElicitation {
|
|
25
|
+
/** Every fragment must occur in exactly one actual form request. Never sent to the model. */
|
|
26
|
+
messageIncludes: readonly string[];
|
|
27
|
+
response: { action: "accept" | "decline" | "cancel"; content?: Record<string, unknown> };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ElicitationEvidence {
|
|
31
|
+
readonly mode: "form";
|
|
32
|
+
readonly message: string;
|
|
33
|
+
readonly requestedSchema: Record<string, unknown>;
|
|
34
|
+
readonly response?: ScriptedElicitation["response"];
|
|
35
|
+
readonly inputSource?: "scripted-human";
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
export interface SemanticConversation {
|
|
24
39
|
/** Sends this exact user message through the same provider-native MCP conversation. */
|
|
25
|
-
send(prompt: string): Promise<ConversationTurn>;
|
|
40
|
+
send(prompt: string, options?: { elicitations: readonly ScriptedElicitation[] }): Promise<ConversationTurn>;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
export function createConversation(
|
|
@@ -39,7 +54,11 @@ export function assertToolCallsWithOptionalFeedback(
|
|
|
39
54
|
export function assertNoToolCalls(turn: ConversationTurn): void;
|
|
40
55
|
/** Allows no tool call or exactly one call to the named tool in each trial. */
|
|
41
56
|
export function assertOptionalToolCall(turn: ConversationTurn, name: string): void;
|
|
42
|
-
|
|
57
|
+
/** Requires an exact sequence, or one of at most eight explicitly authored sequences. */
|
|
58
|
+
export function assertToolNames(
|
|
59
|
+
turn: ConversationTurn,
|
|
60
|
+
expected: readonly string[] | { readonly oneOf: readonly (readonly string[])[] },
|
|
61
|
+
): void;
|
|
43
62
|
export function assertToolArguments(
|
|
44
63
|
turn: ConversationTurn,
|
|
45
64
|
name: string,
|
package/semantic/test.mjs
CHANGED
|
@@ -60,15 +60,19 @@ export async function createConversation(testContext, options) {
|
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
62
|
for (let trial = 1; trial <= 3; trial += 1) {
|
|
63
|
+
const environment = environmentForTrial(specification.environment, trial);
|
|
63
64
|
const running = await startSemanticServer({
|
|
64
65
|
...specification,
|
|
65
|
-
environment
|
|
66
|
+
environment,
|
|
66
67
|
}, testContext.signal);
|
|
67
68
|
try {
|
|
68
69
|
const tools = await listMcpTools(running.url, specification, testContext.signal);
|
|
69
70
|
const record = { trial, turns: [] };
|
|
70
71
|
const directory = await mkdtemp(join(tmpdir(), "emseepea-conversation-"));
|
|
71
|
-
|
|
72
|
+
const serverSecrets = Object.entries(environment ?? {})
|
|
73
|
+
.filter(([key]) => /token|password|secret|credential|api.?key/i.test(key))
|
|
74
|
+
.map(([, value]) => value);
|
|
75
|
+
state.trials.push({ running, tools, record, directory, history: [], model: undefined, serverSecrets });
|
|
72
76
|
evidence.answerTrials.push(record);
|
|
73
77
|
} catch (error) {
|
|
74
78
|
await stopSemanticServer(running.child);
|
|
@@ -82,7 +86,7 @@ export async function createConversation(testContext, options) {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
return Object.freeze({
|
|
85
|
-
async send(prompt) {
|
|
89
|
+
async send(prompt, options) {
|
|
86
90
|
if (typeof prompt !== "string" || !prompt.trim()) throw new Error("send needs a user prompt");
|
|
87
91
|
ensureOpen(state);
|
|
88
92
|
const results = await Promise.allSettled(state.trials.map(async (trial) => {
|
|
@@ -94,8 +98,9 @@ export async function createConversation(testContext, options) {
|
|
|
94
98
|
semanticAuthToken(specification),
|
|
95
99
|
specification.context,
|
|
96
100
|
testContext.signal,
|
|
101
|
+
trial.serverSecrets,
|
|
97
102
|
);
|
|
98
|
-
const answer = await trial.model.send(prompt);
|
|
103
|
+
const answer = await trial.model.send(prompt, options);
|
|
99
104
|
const calls = answer.calls;
|
|
100
105
|
trial.history.push({
|
|
101
106
|
user: prompt,
|
|
@@ -111,6 +116,7 @@ export async function createConversation(testContext, options) {
|
|
|
111
116
|
interactionMode: "native-mcp",
|
|
112
117
|
prompt,
|
|
113
118
|
response: answer.answer,
|
|
119
|
+
elicitations: answer.elicitations,
|
|
114
120
|
toolCalls: calls.map((call, index) => ({
|
|
115
121
|
...call,
|
|
116
122
|
result: answer.toolResults[index].content,
|
|
@@ -153,6 +159,7 @@ export async function createConversation(testContext, options) {
|
|
|
153
159
|
}
|
|
154
160
|
const trial = state.trials[index];
|
|
155
161
|
trial.record.error = safeModelFailure(result.reason);
|
|
162
|
+
if (Array.isArray(result.reason?.elicitations)) trial.record.elicitations = result.reason.elicitations;
|
|
156
163
|
if (Array.isArray(result.reason?.attemptedToolCalls)) {
|
|
157
164
|
trial.record.attemptedToolCalls = result.reason.attemptedToolCalls;
|
|
158
165
|
}
|
|
@@ -165,6 +172,7 @@ export async function createConversation(testContext, options) {
|
|
|
165
172
|
return Object.freeze({
|
|
166
173
|
responses: Object.freeze(trials.map(({ answer }) => answer)),
|
|
167
174
|
toolCalls: Object.freeze(trials.map(({ calls }) => Object.freeze(calls))),
|
|
175
|
+
elicitations: Object.freeze(trials.map(({ record }) => Object.freeze(record.elicitations))),
|
|
168
176
|
[privateTurn]: trials,
|
|
169
177
|
});
|
|
170
178
|
},
|
|
@@ -260,15 +268,28 @@ export function assertOptionalToolCall(turn, name) {
|
|
|
260
268
|
|
|
261
269
|
export function assertToolNames(turn, expected) {
|
|
262
270
|
const trials = turnTrials(turn);
|
|
263
|
-
|
|
271
|
+
const validNames = (names) => Array.isArray(names)
|
|
272
|
+
&& names.every((name) => typeof name === "string" && name.trim());
|
|
273
|
+
const alternatives = Array.isArray(expected) ? undefined : expected?.oneOf;
|
|
274
|
+
if (alternatives !== undefined && (!Array.isArray(alternatives)
|
|
275
|
+
|| alternatives.length < 1 || alternatives.length > 8 || !alternatives.every(validNames))) {
|
|
276
|
+
throw new Error("Expected tool alternatives must contain one to eight string arrays");
|
|
277
|
+
}
|
|
278
|
+
if (alternatives === undefined && !validNames(expected)) {
|
|
264
279
|
throw new Error("Expected tool names must be a string array");
|
|
265
280
|
}
|
|
281
|
+
const sequences = (alternatives ?? [expected]).map((names) => [...names]);
|
|
266
282
|
for (const trial of trials) {
|
|
267
|
-
trial.
|
|
283
|
+
const actual = trial.calls.map(({ name }) => name);
|
|
284
|
+
const match = sequences.find((names) => JSON.stringify(names) === JSON.stringify(actual));
|
|
285
|
+
trial.record.expectedTools = match ?? sequences[0];
|
|
286
|
+
if (alternatives !== undefined) trial.record.expectedToolAlternatives = sequences;
|
|
268
287
|
recordFlexibleExpectation(trial.record);
|
|
269
288
|
}
|
|
270
289
|
try {
|
|
271
|
-
for (const trial of trials)
|
|
290
|
+
for (const trial of trials) {
|
|
291
|
+
assert.deepStrictEqual(trial.calls.map(({ name }) => name), trial.record.expectedTools);
|
|
292
|
+
}
|
|
272
293
|
} catch {
|
|
273
294
|
failAssertion(trials, "tool-name assertion");
|
|
274
295
|
throw new Error("Tool names did not match the expected order and count");
|
|
@@ -457,6 +478,12 @@ function safeModelFailure(error) {
|
|
|
457
478
|
"Model conversation could not start",
|
|
458
479
|
"Model conversation is closed",
|
|
459
480
|
"Model conversation was cancelled",
|
|
481
|
+
"Unexpected native control request",
|
|
482
|
+
"Unsupported native elicitation request",
|
|
483
|
+
"Unexpected or duplicate native elicitation",
|
|
484
|
+
"Unused scripted elicitation response",
|
|
485
|
+
"Invalid scripted elicitations",
|
|
486
|
+
"Invalid scripted elicitation",
|
|
460
487
|
]);
|
|
461
488
|
if (safeMessages.has(message) || /^Model (?:command|conversation) exited \d{1,3}$/.test(message)) {
|
|
462
489
|
return message.replace(/^./, (character) => character.toLowerCase());
|
|
@@ -538,6 +565,8 @@ function recordFlexibleExpectation(record) {
|
|
|
538
565
|
tools: record.expectedTools,
|
|
539
566
|
arguments: record.expectedArguments,
|
|
540
567
|
feedback: record.expectedFeedback,
|
|
568
|
+
...(record.expectedToolAlternatives === undefined
|
|
569
|
+
? {} : { alternatives: record.expectedToolAlternatives }),
|
|
541
570
|
}));
|
|
542
571
|
}
|
|
543
572
|
|