@emseepea/testing 0.21.7 → 0.22.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 CHANGED
@@ -57,6 +57,12 @@ advertised-tool text, an answer wrapper, or prepared MCP material. Exact tool
57
57
  assertions come from the provider's native MCP events. Follow-up messages use
58
58
  the same conversation.
59
59
 
60
+ Native conversations have no framework tool-call or native provider-turn count
61
+ limit. Calls must still use advertised MCP tools. The per-send timeout,
62
+ cancellation, output checks, model checks, and isolated test environment remain
63
+ effective. Exact application assertions decide which calls and results are
64
+ acceptable. Tool-free judge invocations retain their existing limits.
65
+
60
66
  Call `await conversation.fresh()` to discard provider transcript history while
61
67
  keeping the same running test application and its saved data. The next `send`
62
68
  starts a new provider-native session in each trial.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/testing",
3
- "version": "0.21.7",
3
+ "version": "0.22.0",
4
4
  "description": "MCP integration and semantic testing helpers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,7 +46,8 @@
46
46
  "build": "tsc -p tsconfig.json",
47
47
  "prepack": "npm run build",
48
48
  "test": "npm run build && npm run test:built",
49
- "test:built": "node --test test/*.test.mjs"
49
+ "test:built": "node --test test/*.test.mjs",
50
+ "test:llm:built": "emseepea-test eval"
50
51
  },
51
52
  "dependencies": {
52
53
  "@emseepea/server": "0.23.0",
package/semantic/cli.mjs CHANGED
@@ -1,23 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { createHash } from "node:crypto";
4
3
  import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
4
  import { tmpdir } from "node:os";
6
5
  import { dirname, join, relative, resolve } from "node:path";
6
+ import { validRecord } from "./evidence.mjs";
7
7
  import { discoverTests } from "./discover.mjs";
8
8
  import { modelVersion, providerModel, providerSettings } from "./provider.mjs";
9
9
 
10
- const negativeFeedbackObservations = new Set([
11
- "error",
12
- "friction",
13
- "annoyance",
14
- "unnecessary_difficulty",
15
- "confusion",
16
- "repetition",
17
- "unexpected_bad_result",
18
- "capability_mismatch",
19
- ]);
20
-
21
10
  const paths = [];
22
11
  let provider = process.env.EMSEEPEA_EVAL_PROVIDER ?? "claude-local";
23
12
  let output = "artifacts/llm-eval/evidence.json";
@@ -101,7 +90,7 @@ try {
101
90
  for (const file of files) {
102
91
  const displayFile = relative(process.cwd(), file);
103
92
  const cases = Object.values(evidence.cases).filter((record) => record.file === displayFile);
104
- if (!cases.length || cases.some((record) => !validRecord(record, evidence.authoritative, smoke))) {
93
+ if (!cases.length || cases.some((record) => !validRecord(record, evidence))) {
105
94
  evidence.errors.push(`Missing or failed qualification: ${displayFile}`);
106
95
  }
107
96
  }
@@ -114,138 +103,3 @@ try {
114
103
  }
115
104
  if (evidence.status !== "passed") process.exitCode = 1;
116
105
  console.log(`Semantic checks ${evidence.status}; evidence: ${output}`);
117
-
118
- function validRecord(record, authoritative, smoke) {
119
- const isHash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
120
- if (record.status !== "passed" || record.authoritative !== authoritative || record.smoke !== smoke
121
- || record.provider !== evidence.provider || record.configuredModel !== evidence.configuredModel
122
- || record.modelEvidence !== evidence.modelEvidence
123
- || JSON.stringify(record.settings) !== JSON.stringify(evidence.settings)
124
- || record.mode !== "conversation" || record.answerTrials?.length !== 3
125
- || !Number.isInteger(record.judgeVerdicts?.length) || record.judgeVerdicts.length < 9
126
- || record.judgeVerdicts.length % 3 !== 0
127
- || !record.judgeVerdicts.every((judgment) => isHash(judgment.expectationSha256)
128
- && isHash(judgment.requestSha256) && isHash(judgment.responseSha256)
129
- && typeof judgment.expectedMeaning === "string" && judgment.expectedMeaning.length > 0
130
- && judgment.verdict?.pass === true && judgment.verdict.score === 1
131
- && typeof judgment.verdict.reason === "string" && judgment.verdict.reason.length > 0)) return false;
132
- return record.answerTrials.every((trial) => Array.isArray(trial.turns) && trial.turns.length > 0
133
- && trial.turns.every((turn) => Number.isInteger(turn.advertisedToolCount)
134
- && turn.advertisedToolCount >= 0
135
- && turn.interactionMode === "native-mcp"
136
- && turn.answerTurnCount === 1
137
- && turn.answerProviderToolCount === turn.toolCallCount
138
- && turn.answerProviderTurnCount === turn.toolCallCount + 1
139
- && Number.isInteger(turn.toolCallCount) && turn.toolCallCount >= 0 && turn.toolCallCount <= 3
140
- && typeof turn.prompt === "string" && turn.prompt.length > 0
141
- && typeof turn.response === "string"
142
- && isHash(turn.promptSha256) && isHash(turn.answerSha256)
143
- && isHash(turn.advertisedToolsSha256) && isHash(turn.selectedCallsSha256)
144
- && Array.isArray(turn.toolCalls)
145
- && JSON.stringify(turn.selectedTools)
146
- === JSON.stringify(turn.toolCalls.map(({ name }) => name))
147
- && validToolAssertions(turn, isHash)
148
- && validNegativeFeedbackAssertion(turn)
149
- && validFeedbackDisclosure(turn, record.judgeVerdicts, trial.trial, isHash)
150
- && turn.toolCalls.every((call) => Object.hasOwn(call, "result") && typeof call.isError === "boolean")
151
- && Array.isArray(turn.pathEvidence) && turn.pathEvidence.length === turn.toolCallCount
152
- && turn.pathEvidence.every(({ method, target, requestSha256, responseSha256 }) =>
153
- method === "tools/call" && turn.selectedTools.includes(target)
154
- && isHash(requestSha256) && isHash(responseSha256))));
155
- }
156
-
157
- function validToolAssertions(turn, isHash) {
158
- if (turn.expectedOptionalFeedback === true) {
159
- if (!Array.isArray(turn.expectedCalls)) return false;
160
- const expectedHash = createHash("sha256").update(JSON.stringify({
161
- calls: turn.expectedCalls,
162
- optionalFeedback: true,
163
- })).digest("hex");
164
- const calls = turn.toolCalls.map(({ name, arguments: args }) => ({ name, arguments: args }));
165
- const primaryCalls = calls.slice(0, turn.expectedCalls?.length);
166
- const trailingCalls = calls.slice(turn.expectedCalls?.length);
167
- return expectedHash === turn.expectedSelectionSha256
168
- && JSON.stringify(primaryCalls) === JSON.stringify(turn.expectedCalls)
169
- && (trailingCalls.length === 0
170
- || (trailingCalls.length === 1 && trailingCalls[0].name === "submit-feedback"));
171
- }
172
- if (typeof turn.expectedOptionalTool === "string" && turn.expectedOptionalTool.trim()) {
173
- const expectedHash = createHash("sha256").update(JSON.stringify({
174
- optionalTool: turn.expectedOptionalTool,
175
- })).digest("hex");
176
- return expectedHash === turn.expectedSelectionSha256
177
- && (turn.toolCalls.length === 0
178
- || (turn.toolCalls.length === 1 && turn.toolCalls[0].name === turn.expectedOptionalTool));
179
- }
180
- if (JSON.stringify(turn.selectedTools) !== JSON.stringify(turn.expectedTools)) return false;
181
- if (turn.expectedToolAlternatives !== undefined) {
182
- const alternatives = turn.expectedToolAlternatives;
183
- if (!Array.isArray(alternatives) || alternatives.length < 1 || alternatives.length > 8
184
- || !alternatives.every((names) => Array.isArray(names)
185
- && names.every((name) => typeof name === "string" && name.trim()))
186
- || !alternatives.some((names) => JSON.stringify(names) === JSON.stringify(turn.expectedTools))) {
187
- return false;
188
- }
189
- }
190
- if (isHash(turn.expectedCallsSha256)) {
191
- return JSON.stringify(turn.toolCalls.map(({ name, arguments: args }) => ({ name, arguments: args })))
192
- === JSON.stringify(turn.expectedCalls);
193
- }
194
- if (!isHash(turn.expectedSelectionSha256)) return false;
195
- const expectedHash = createHash("sha256").update(JSON.stringify({
196
- tools: turn.expectedTools,
197
- arguments: turn.expectedArguments,
198
- feedback: turn.expectedFeedback,
199
- ...(turn.expectedToolAlternatives === undefined
200
- ? {} : { alternatives: turn.expectedToolAlternatives }),
201
- })).digest("hex");
202
- if (expectedHash !== turn.expectedSelectionSha256) return false;
203
- for (const [name, expected] of Object.entries(turn.expectedArguments ?? {})) {
204
- const matches = turn.toolCalls.filter((call) => call.name === name);
205
- if (matches.length !== 1 || JSON.stringify(matches[0].arguments) !== JSON.stringify(expected)) return false;
206
- }
207
- if (turn.expectedFeedback) {
208
- const calls = turn.toolCalls.filter((call) => call.name === "submit-feedback");
209
- const detail = calls[0]?.arguments?.detail;
210
- if (calls.length !== 1 || !turn.expectedFeedback.observation.includes(calls[0].arguments?.observation)
211
- || typeof detail !== "string" || turn.expectedFeedback.detailIncludes.some(
212
- (value) => !detail.toLowerCase().includes(value.toLowerCase()),
213
- )) return false;
214
- }
215
- return true;
216
- }
217
-
218
- function validNegativeFeedbackAssertion(turn) {
219
- if (turn.expectedNegativeFeedback !== false) return true;
220
- const actual = turn.toolCalls.filter((call) => call.name === "submit-feedback"
221
- && negativeFeedbackObservations.has(call.arguments?.observation));
222
- return actual.length === 0 && JSON.stringify(actual) === JSON.stringify(turn.negativeFeedbackCalls);
223
- }
224
-
225
- function validFeedbackDisclosure(turn, judgments, trial, isHash) {
226
- if (turn.expectedOptionalFeedback !== true) return turn.feedbackDisclosure === undefined;
227
- const feedback = turn.toolCalls[turn.expectedCalls.length];
228
- if (!feedback) return turn.feedbackDisclosure === undefined;
229
- if (feedback.isError || typeof feedback.arguments?.observation !== "string"
230
- || !feedback.arguments.observation.trim() || typeof feedback.arguments.detail !== "string"
231
- || !feedback.arguments.detail.trim()) return false;
232
- const expectedMeaning = feedbackDisclosureExpectation(feedback);
233
- const expectationSha256 = createHash("sha256").update(expectedMeaning).digest("hex");
234
- if (!isHash(turn.feedbackDisclosure?.expectationSha256)
235
- || JSON.stringify(turn.feedbackDisclosure) !== JSON.stringify({
236
- observation: feedback.arguments.observation,
237
- detail: feedback.arguments.detail,
238
- expectationSha256,
239
- })) return false;
240
- const matching = judgments.filter((judgment) => judgment.trial === trial
241
- && judgment.turn === turn.turn && judgment.expectationSha256 === expectationSha256);
242
- return matching.length === 3 && matching.every((judgment) =>
243
- judgment.expectedMeaning === expectedMeaning && judgment.verdict?.pass === true);
244
- }
245
-
246
- function feedbackDisclosureExpectation(call) {
247
- return "The final assistant response makes it clear that feedback was submitted, recorded, or noted, "
248
- + "and communicates the substance "
249
- + `of this specific ${call.arguments.observation} observation: ${call.arguments.detail} `
250
- + "A concise, faithful summary is enough. It need not repeat every example, field, or phrase.";
251
- }
@@ -0,0 +1,148 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const negativeFeedbackObservations = new Set([
4
+ "error",
5
+ "friction",
6
+ "annoyance",
7
+ "unnecessary_difficulty",
8
+ "confusion",
9
+ "repetition",
10
+ "unexpected_bad_result",
11
+ "capability_mismatch",
12
+ ]);
13
+
14
+ export function validRecord(record, evidence) {
15
+ const { authoritative, smoke } = evidence;
16
+ const isHash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
17
+ if (record.status !== "passed" || record.authoritative !== authoritative || record.smoke !== smoke
18
+ || record.provider !== evidence.provider || record.configuredModel !== evidence.configuredModel
19
+ || record.modelEvidence !== evidence.modelEvidence
20
+ || JSON.stringify(record.settings) !== JSON.stringify(evidence.settings)
21
+ || record.mode !== "conversation" || record.answerTrials?.length !== 3
22
+ || !Number.isInteger(record.judgeVerdicts?.length) || record.judgeVerdicts.length < 9
23
+ || record.judgeVerdicts.length % 3 !== 0
24
+ || !record.judgeVerdicts.every((judgment) => isHash(judgment.expectationSha256)
25
+ && isHash(judgment.requestSha256) && isHash(judgment.responseSha256)
26
+ && typeof judgment.expectedMeaning === "string" && judgment.expectedMeaning.length > 0
27
+ && judgment.verdict?.pass === true && judgment.verdict.score === 1
28
+ && typeof judgment.verdict.reason === "string" && judgment.verdict.reason.length > 0)) return false;
29
+ return record.answerTrials.every((trial) => Array.isArray(trial.turns) && trial.turns.length > 0
30
+ && trial.turns.every((turn) => Number.isInteger(turn.advertisedToolCount)
31
+ && turn.advertisedToolCount >= 0
32
+ && turn.interactionMode === "native-mcp"
33
+ && turn.answerTurnCount === 1
34
+ && turn.answerProviderToolCount === turn.toolCallCount
35
+ && Number.isInteger(turn.answerProviderTurnCount) && turn.answerProviderTurnCount >= 1
36
+ && Number.isInteger(turn.toolCallCount) && turn.toolCallCount >= 0
37
+ && typeof turn.prompt === "string" && turn.prompt.length > 0
38
+ && typeof turn.response === "string"
39
+ && isHash(turn.promptSha256) && isHash(turn.answerSha256)
40
+ && isHash(turn.advertisedToolsSha256) && isHash(turn.selectedCallsSha256)
41
+ && Array.isArray(turn.toolCalls) && turn.toolCalls.length === turn.toolCallCount
42
+ && JSON.stringify(turn.selectedTools)
43
+ === JSON.stringify(turn.toolCalls.map(({ name }) => name))
44
+ && validToolAssertions(turn, isHash)
45
+ && validNegativeFeedbackAssertion(turn)
46
+ && validFeedbackDisclosure(turn, record.judgeVerdicts, trial.trial, isHash)
47
+ && turn.toolCalls.every((call) => Object.hasOwn(call, "result") && typeof call.isError === "boolean")
48
+ && Array.isArray(turn.pathEvidence) && turn.pathEvidence.length === turn.toolCallCount
49
+ && turn.pathEvidence.every(({ method, target, requestSha256, responseSha256 }) =>
50
+ method === "tools/call" && turn.selectedTools.includes(target)
51
+ && isHash(requestSha256) && isHash(responseSha256))));
52
+ }
53
+
54
+ function validToolAssertions(turn, isHash) {
55
+ if (turn.expectedOptionalFeedback === true) {
56
+ if (!Array.isArray(turn.expectedCalls)) return false;
57
+ const expectedHash = createHash("sha256").update(JSON.stringify({
58
+ calls: turn.expectedCalls,
59
+ optionalFeedback: true,
60
+ })).digest("hex");
61
+ const calls = turn.toolCalls.map(({ name, arguments: args }) => ({ name, arguments: args }));
62
+ const primaryCalls = calls.slice(0, turn.expectedCalls?.length);
63
+ const trailingCalls = calls.slice(turn.expectedCalls?.length);
64
+ return expectedHash === turn.expectedSelectionSha256
65
+ && JSON.stringify(primaryCalls) === JSON.stringify(turn.expectedCalls)
66
+ && (trailingCalls.length === 0
67
+ || (trailingCalls.length === 1 && trailingCalls[0].name === "submit-feedback"));
68
+ }
69
+ if (typeof turn.expectedOptionalTool === "string" && turn.expectedOptionalTool.trim()) {
70
+ const expectedHash = createHash("sha256").update(JSON.stringify({
71
+ optionalTool: turn.expectedOptionalTool,
72
+ })).digest("hex");
73
+ return expectedHash === turn.expectedSelectionSha256
74
+ && (turn.toolCalls.length === 0
75
+ || (turn.toolCalls.length === 1 && turn.toolCalls[0].name === turn.expectedOptionalTool));
76
+ }
77
+ if (JSON.stringify(turn.selectedTools) !== JSON.stringify(turn.expectedTools)) return false;
78
+ if (turn.expectedToolAlternatives !== undefined) {
79
+ const alternatives = turn.expectedToolAlternatives;
80
+ if (!Array.isArray(alternatives) || alternatives.length < 1 || alternatives.length > 8
81
+ || !alternatives.every((names) => Array.isArray(names)
82
+ && names.every((name) => typeof name === "string" && name.trim()))
83
+ || !alternatives.some((names) => JSON.stringify(names) === JSON.stringify(turn.expectedTools))) {
84
+ return false;
85
+ }
86
+ }
87
+ if (isHash(turn.expectedCallsSha256)) {
88
+ return JSON.stringify(turn.toolCalls.map(({ name, arguments: args }) => ({ name, arguments: args })))
89
+ === JSON.stringify(turn.expectedCalls);
90
+ }
91
+ if (!isHash(turn.expectedSelectionSha256)) return false;
92
+ const expectedHash = createHash("sha256").update(JSON.stringify({
93
+ tools: turn.expectedTools,
94
+ arguments: turn.expectedArguments,
95
+ feedback: turn.expectedFeedback,
96
+ ...(turn.expectedToolAlternatives === undefined
97
+ ? {} : { alternatives: turn.expectedToolAlternatives }),
98
+ })).digest("hex");
99
+ if (expectedHash !== turn.expectedSelectionSha256) return false;
100
+ for (const [name, expected] of Object.entries(turn.expectedArguments ?? {})) {
101
+ const matches = turn.toolCalls.filter((call) => call.name === name);
102
+ if (matches.length !== 1 || JSON.stringify(matches[0].arguments) !== JSON.stringify(expected)) return false;
103
+ }
104
+ if (turn.expectedFeedback) {
105
+ const calls = turn.toolCalls.filter((call) => call.name === "submit-feedback");
106
+ const detail = calls[0]?.arguments?.detail;
107
+ if (calls.length !== 1 || !turn.expectedFeedback.observation.includes(calls[0].arguments?.observation)
108
+ || typeof detail !== "string" || turn.expectedFeedback.detailIncludes.some(
109
+ (value) => !detail.toLowerCase().includes(value.toLowerCase()),
110
+ )) return false;
111
+ }
112
+ return true;
113
+ }
114
+
115
+ function validNegativeFeedbackAssertion(turn) {
116
+ if (turn.expectedNegativeFeedback !== false) return true;
117
+ const actual = turn.toolCalls.filter((call) => call.name === "submit-feedback"
118
+ && negativeFeedbackObservations.has(call.arguments?.observation));
119
+ return actual.length === 0 && JSON.stringify(actual) === JSON.stringify(turn.negativeFeedbackCalls);
120
+ }
121
+
122
+ function validFeedbackDisclosure(turn, judgments, trial, isHash) {
123
+ if (turn.expectedOptionalFeedback !== true) return turn.feedbackDisclosure === undefined;
124
+ const feedback = turn.toolCalls[turn.expectedCalls.length];
125
+ if (!feedback) return turn.feedbackDisclosure === undefined;
126
+ if (feedback.isError || typeof feedback.arguments?.observation !== "string"
127
+ || !feedback.arguments.observation.trim() || typeof feedback.arguments.detail !== "string"
128
+ || !feedback.arguments.detail.trim()) return false;
129
+ const expectedMeaning = feedbackDisclosureExpectation(feedback);
130
+ const expectationSha256 = createHash("sha256").update(expectedMeaning).digest("hex");
131
+ if (!isHash(turn.feedbackDisclosure?.expectationSha256)
132
+ || JSON.stringify(turn.feedbackDisclosure) !== JSON.stringify({
133
+ observation: feedback.arguments.observation,
134
+ detail: feedback.arguments.detail,
135
+ expectationSha256,
136
+ })) return false;
137
+ const matching = judgments.filter((judgment) => judgment.trial === trial
138
+ && judgment.turn === turn.turn && judgment.expectationSha256 === expectationSha256);
139
+ return matching.length === 3 && matching.every((judgment) =>
140
+ judgment.expectedMeaning === expectedMeaning && judgment.verdict?.pass === true);
141
+ }
142
+
143
+ function feedbackDisclosureExpectation(call) {
144
+ return "The final assistant response makes it clear that feedback was submitted, recorded, or noted, "
145
+ + "and communicates the substance "
146
+ + `of this specific ${call.arguments.observation} observation: ${call.arguments.detail} `
147
+ + "A concise, faithful summary is enough. It need not repeat every example, field, or phrase.";
148
+ }
@@ -50,7 +50,7 @@ export function providerSettings(provider) {
50
50
  userConfig: "ignored",
51
51
  webSearch: false,
52
52
  }
53
- : { effort: "low", maxTurns: 4, permissionMode: "dontAsk", userSettings: false };
53
+ : { effort: "low", maxTurns: null, judgeMaxTurns: 4, permissionMode: "dontAsk", userSettings: false };
54
54
  }
55
55
 
56
56
  export function parseClaudeEvents(stdout, processExitCode = 0) {
@@ -115,11 +115,6 @@ export function parseNativeClaudeEvents(stdout, advertisedTools, requireInit = f
115
115
  }
116
116
  return { name: publicName, arguments: input };
117
117
  });
118
- if (toolUses.length > 3) {
119
- throw Object.assign(new Error("Model command used more than three tools"), {
120
- attemptedToolCalls: calls,
121
- });
122
- }
123
118
  if (init) {
124
119
  const available = [...(init.tools ?? [])].sort();
125
120
  const expected = [...advertised.keys()].sort();
@@ -156,9 +151,8 @@ export function parseNativeClaudeEvents(stdout, advertisedTools, requireInit = f
156
151
  if ((result.permission_denials?.length ?? 0) > 0) {
157
152
  throw new Error("Model command attempted a forbidden action");
158
153
  }
159
- if (!Number.isInteger(result.num_turns)) throw new Error("Model command returned an invalid turn count");
160
- if (result.num_turns !== toolUses.length + 1) {
161
- throw new Error("Model command used an unexpected number of turns");
154
+ if (!Number.isInteger(result.num_turns) || result.num_turns < 1) {
155
+ throw new Error("Model command returned an invalid turn count");
162
156
  }
163
157
  const usage = result.modelUsage?.[claudeModel];
164
158
  if (usage?.canonicalModel !== claudeModel || usage.provider !== "firstParty") {
@@ -251,7 +245,6 @@ export function conversationInvocation(provider, directory, url, tools, authToke
251
245
  "--verbose",
252
246
  "--model", claudeModel,
253
247
  "--effort", "low",
254
- "--max-turns", "4",
255
248
  "--strict-mcp-config",
256
249
  ...(config ? ["--mcp-config", JSON.stringify(config)] : []),
257
250
  "--disable-slash-commands",
@@ -444,7 +437,6 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
444
437
  const forbidden = completed.filter((item) => !["agent_message", "reasoning", "mcp_tool_call"].includes(item?.type));
445
438
  if (forbidden.length) throw new Error("Model command used a forbidden tool");
446
439
  const toolItems = completed.filter(({ type }) => type === "mcp_tool_call");
447
- if (toolItems.length > 3) throw new Error("Model command used more than three tools");
448
440
  const calls = toolItems.map((item) => {
449
441
  if (item.server !== mcpServerName || !advertised.has(item.tool)
450
442
  || !item.arguments || typeof item.arguments !== "object" || Array.isArray(item.arguments)) {
package/semantic/test.mjs CHANGED
@@ -496,7 +496,6 @@ function safeModelFailure(error) {
496
496
  "Model command did not resume the required session",
497
497
  "Model command reported a failed MCP tool call",
498
498
  "Model command exposed a configured secret",
499
- "Model command used more than three tools",
500
499
  "Model conversation already has a pending turn",
501
500
  "Model conversation could not start",
502
501
  "Model conversation is closed",