start-command 0.41.0 → 0.43.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/CHANGELOG.md +23 -0
- package/package.json +3 -3
- package/src/lib/formal-ai-client.js +249 -22
- package/src/lib/formal-ai-files.js +797 -0
- package/src/lib/formal-ai-policy.js +10 -3
- package/src/lib/formal-ai.js +328 -12
- package/src/lib/log-sanitizer.js +4 -0
- package/src/lib/usage.js +3 -1
- package/test/fixtures/formal-ai/receipts-258.json +449 -0
- package/test/fixtures/formal-ai/receipts-261.json +589 -0
- package/test/fixtures/formal-ai/replay.cjs +60 -7
- package/test/regression-255-cli.js +3 -1
- package/test/regression-255-e2e.js +80 -3
- package/test/regression-255-loop.js +6 -1
- package/test/regression-255-policy.js +1 -1
- package/test/regression-257.js +265 -0
- package/test/regression-258-files.js +452 -0
- package/test/regression-258.js +456 -0
- package/test/regression-261.js +856 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.43.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8567804: `--use-formal-ai`: bounded typed source edits (#261).
|
|
8
|
+
|
|
9
|
+
- **Bound preimage:** an `edit` or `write` of an existing file is bound to the complete bytes the `read` tool returned for that exact path in the same task. `cat`, a directory listing or a stale read is not enough, and a change of the target while start writes is refused. `read` returns the text as the numbered frame Formal AI binds as a whole-file Read and decodes back exactly.
|
|
10
|
+
- **Owned change only:** `old_string` must occur exactly once (or `replace_all` is set), and only those byte regions change. Unicode, CRLF, a byte order mark and a missing final newline are preserved, and symlinks are followed only inside the working directory.
|
|
11
|
+
- **Receipt and verification:** each change keeps a mutation receipt (`tool`, `target`, `preimage`, `postimage`, byte `matches`, `readback`) in the evidence and returns it to Formal AI. The readback is verified byte for byte against the preimage and the matches, independently of fingerprints; any other byte change ends the run with 70.
|
|
12
|
+
- **Unresolved failures:** a failed `read`, `edit`, `write` or `bash` change stays open until a verified change of the same target (or the same command) resolves it. A later read, a `cp`/`mv` over the file or Formal AI's answer does not resolve it, and the run ends `incomplete` with the original failure and its exit status.
|
|
13
|
+
- **Gap records:** `SourcePreimageUnavailable` is a known `workspace-change-verification-gap/v1` reason. Other JSON gap records and Formal AI's Links Notation `obligation_gap` answers end `incomplete` with exit 65 instead of success, unless they only repeat tool output (including a read frame's text). There is still no shell or vendor fallback.
|
|
14
|
+
- **Dependency:** log sanitization now depends on `@link-foundation/sensitive-data-sanitizer` ^0.14.0, its current release.
|
|
15
|
+
|
|
16
|
+
## 0.42.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 5ebb7ec: `--use-formal-ai`: keep the cause of Formal AI server errors, offer structured file tools, and report capability gaps as incomplete (#257, #258, #259).
|
|
21
|
+
|
|
22
|
+
- **Server errors (#257):** an HTTP error from the Formal AI runtime now keeps its sanitized, bounded cause: the error type (100 chars), the message (300), a body preview (500), the body length and the number of attempts. The cause appears in the final message (`Formal AI returned HTTP 500: formal_ai_error: Aborted(...) (after 1 retry)`) and under `outcome.diagnostics` in the evidence. Authorization values, credentials, control characters and the environment are never recorded. There is still no fallback, the exit status is still 70, and 4xx responses are not retried.
|
|
23
|
+
- **File tools (#258):** next to `bash`, Formal AI is offered a structured `read` tool, plus `edit` (one literal replacement) and `write` (replace a file it has read) in `START_FORMAL_AI_EFFECTS=workspace-write` mode. The tools follow the workspace path policy: no paths outside the working directory, and a change only to a file read in the same task that has not changed since. They keep comments, whitespace, line endings, a byte order mark, the file mode and all unrelated content. Each change is checked by reading the file back, and the evidence records its hashes and effects. Files over `START_FORMAL_AI_MAX_FILE_BYTES` (1 MiB by default, at most 16 MiB) are refused, and interpreters are still not offered.
|
|
24
|
+
- **Capability gaps (#258):** an answer that matches one of Formal AI's known capability-gap records (`workspace-edit-capability-gap/v1` or `workspace-change-verification-gap/v1`, with `complete` and `verified` both `false`) is now reported as `capability-gap` with exit 65. Its remaining obligations are kept. Plain answers to questions still exit 0.
|
|
25
|
+
|
|
3
26
|
## 0.41.0
|
|
4
27
|
|
|
5
28
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "start-command",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
|
|
5
5
|
"main": "src/bin/cli.js",
|
|
6
6
|
"exports": {
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
]
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@link-foundation/sensitive-data-sanitizer": "^0.
|
|
71
|
+
"@link-foundation/sensitive-data-sanitizer": "^0.14.0",
|
|
72
72
|
"command-stream": "^2.6.0",
|
|
73
|
-
"lino-objects-codec": "^0.9.
|
|
73
|
+
"lino-objects-codec": "^0.9.5"
|
|
74
74
|
},
|
|
75
75
|
"overrides": {
|
|
76
76
|
"brace-expansion": "^5.0.12",
|
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal client of the OpenAI-compatible agent-mode endpoint of Formal AI
|
|
3
3
|
* (`POST /api/openai/v1/chat/completions`), used by `--use-formal-ai`
|
|
4
|
-
* (issue #255).
|
|
5
|
-
*
|
|
4
|
+
* (issue #255). It offers the `bash` tool and the structured `read` tool, plus
|
|
5
|
+
* `edit` and `write` when workspace changes are allowed (issue #258): Formal
|
|
6
|
+
* AI picks the call, start decides whether and how it runs.
|
|
7
|
+
*
|
|
8
|
+
* An HTTP error keeps a bounded, sanitized excerpt of its body (the error
|
|
9
|
+
* type, message and a preview) and the attempt count, so a failing runtime
|
|
10
|
+
* names its cause (issue #257).
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
const { EXIT } = require('./formal-ai-policy');
|
|
9
14
|
const { API_PREFIX } = require('./formal-ai-runtime');
|
|
15
|
+
const {
|
|
16
|
+
FILE_TOOLS,
|
|
17
|
+
describeFileCall,
|
|
18
|
+
echoedByTool,
|
|
19
|
+
normalizeFileArgs,
|
|
20
|
+
} = require('./formal-ai-files');
|
|
10
21
|
|
|
11
22
|
const { AbortSignal } = globalThis;
|
|
12
23
|
|
|
13
|
-
const
|
|
24
|
+
const BASH_NAMES = new Set(['bash', 'shell', 'run_command']);
|
|
25
|
+
const TOOL_NAMES = new Set([...BASH_NAMES, ...Object.keys(FILE_TOOLS)]);
|
|
14
26
|
const BASH_TOOL = Object.freeze({
|
|
15
27
|
type: 'function',
|
|
16
28
|
function: {
|
|
@@ -26,12 +38,183 @@ const BASH_TOOL = Object.freeze({
|
|
|
26
38
|
});
|
|
27
39
|
|
|
28
40
|
class ProtocolError extends Error {
|
|
29
|
-
constructor(message, exitCode = EXIT.PROTOCOL) {
|
|
41
|
+
constructor(message, exitCode = EXIT.PROTOCOL, details = null) {
|
|
30
42
|
super(message);
|
|
31
43
|
this.exitCode = exitCode;
|
|
44
|
+
this.details = details;
|
|
32
45
|
}
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Tools offered to Formal AI: the change tools only when the effect mode
|
|
50
|
+
* allows changes, so the advertised contract matches the policy.
|
|
51
|
+
* @param {string} effects - Effect mode
|
|
52
|
+
* @returns {object[]}
|
|
53
|
+
*/
|
|
54
|
+
function offeredTools(effects) {
|
|
55
|
+
return effects === 'workspace-write'
|
|
56
|
+
? [BASH_TOOL, FILE_TOOLS.read, FILE_TOOLS.edit, FILE_TOOLS.write]
|
|
57
|
+
: [BASH_TOOL, FILE_TOOLS.read];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PREVIEW_CHARS = 500;
|
|
61
|
+
const MESSAGE_CHARS = 300;
|
|
62
|
+
const REDACTED = '[REDACTED]';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One line of untrusted diagnostic text, bounded and without credentials:
|
|
66
|
+
* known secrets (the bearer token start sent), authorization headers, bearer
|
|
67
|
+
* or basic credentials and credential-named fields are masked, and control
|
|
68
|
+
* characters become spaces.
|
|
69
|
+
* @param {string} text - Diagnostic text from the runtime
|
|
70
|
+
* @param {number} limit - Maximum characters kept
|
|
71
|
+
* @param {string[]} [secrets] - Values that must never appear
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function sanitizeDiagnostic(text, limit, secrets = []) {
|
|
75
|
+
let clean = String(text);
|
|
76
|
+
for (const secret of secrets) {
|
|
77
|
+
if (secret) {
|
|
78
|
+
clean = clean.split(secret).join(REDACTED);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
clean = clean
|
|
82
|
+
.replace(
|
|
83
|
+
/((?:proxy-)?authorization["']?\s*[:=]\s*["']?)[^"'\r\n,;}]*/gi,
|
|
84
|
+
`$1${REDACTED}`
|
|
85
|
+
)
|
|
86
|
+
.replace(/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{4,}/gi, `$1 ${REDACTED}`)
|
|
87
|
+
.replace(
|
|
88
|
+
/((?:api[_-]?key|access[_-]?token|token|secret|password|passwd)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
|
|
89
|
+
`$1${REDACTED}`
|
|
90
|
+
)
|
|
91
|
+
// eslint-disable-next-line no-control-regex
|
|
92
|
+
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
93
|
+
.trim();
|
|
94
|
+
return clean.length > limit
|
|
95
|
+
? `${clean.slice(0, limit)}… [${clean.length - limit} more chars]`
|
|
96
|
+
: clean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Structured diagnostics of an HTTP error response.
|
|
101
|
+
* @param {number} status - HTTP status
|
|
102
|
+
* @param {string} text - Response body
|
|
103
|
+
* @param {string[]} [secrets] - Values to mask
|
|
104
|
+
* @returns {{httpStatus: number, errorType: string|null, errorMessage: string|null, bodyPreview: string, bodyChars: number}}
|
|
105
|
+
*/
|
|
106
|
+
function httpErrorDetails(status, text, secrets = []) {
|
|
107
|
+
let type = null;
|
|
108
|
+
let message = null;
|
|
109
|
+
try {
|
|
110
|
+
const body = JSON.parse(text);
|
|
111
|
+
const error = body && typeof body === 'object' ? body.error : null;
|
|
112
|
+
if (error && typeof error === 'object') {
|
|
113
|
+
type = typeof error.type === 'string' ? error.type : null;
|
|
114
|
+
message = typeof error.message === 'string' ? error.message : null;
|
|
115
|
+
} else if (typeof error === 'string') {
|
|
116
|
+
message = error;
|
|
117
|
+
} else if (body && typeof body.message === 'string') {
|
|
118
|
+
message = body.message;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Not JSON: the preview carries the body.
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
httpStatus: status,
|
|
125
|
+
errorType: type === null ? null : sanitizeDiagnostic(type, 100, secrets),
|
|
126
|
+
errorMessage:
|
|
127
|
+
message === null
|
|
128
|
+
? null
|
|
129
|
+
: sanitizeDiagnostic(message, MESSAGE_CHARS, secrets),
|
|
130
|
+
bodyPreview: sanitizeDiagnostic(text, PREVIEW_CHARS, secrets),
|
|
131
|
+
bodyChars: text.length,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function httpErrorMessage(details) {
|
|
136
|
+
const cause = details.errorMessage
|
|
137
|
+
? `${details.errorType ? `${details.errorType}: ` : ''}${details.errorMessage}`
|
|
138
|
+
: details.bodyPreview.slice(0, MESSAGE_CHARS) || '(empty body)';
|
|
139
|
+
return `Formal AI returned HTTP ${details.httpStatus}: ${cause}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Known structured gaps Formal AI answers instead of claiming success. */
|
|
143
|
+
const GAP_SCHEMAS = Object.freeze({
|
|
144
|
+
'workspace-edit-capability-gap/v1': [
|
|
145
|
+
'EditToolUnavailable',
|
|
146
|
+
'ReadToolUnavailable',
|
|
147
|
+
],
|
|
148
|
+
// SourcePreimageUnavailable: Formal AI could not bind the complete Read
|
|
149
|
+
// before its edit to the change (issue #261).
|
|
150
|
+
'workspace-change-verification-gap/v1': [
|
|
151
|
+
'VerificationRunToolUnavailable',
|
|
152
|
+
'SourcePreimageUnavailable',
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
const CAPABILITIES = new Set([
|
|
156
|
+
'read',
|
|
157
|
+
'edit',
|
|
158
|
+
'multi_edit',
|
|
159
|
+
'write',
|
|
160
|
+
'run',
|
|
161
|
+
'bash',
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Recognize a final answer that is exactly a known Formal AI capability gap.
|
|
166
|
+
* Only the runtime's own answer counts: text that a tool result in this run
|
|
167
|
+
* contained (file data echoed back) is never taken as a gap.
|
|
168
|
+
* @param {string} content - Final answer content
|
|
169
|
+
* @param {object[]} [messages] - Conversation so far
|
|
170
|
+
* @returns {object|null} {schema, target, reason, missingCapabilities}
|
|
171
|
+
*/
|
|
172
|
+
function recognizeCapabilityGap(content, messages = []) {
|
|
173
|
+
const text = typeof content === 'string' ? content.trim() : '';
|
|
174
|
+
if (!text.startsWith('{') || !text.endsWith('}')) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
let gap;
|
|
178
|
+
try {
|
|
179
|
+
gap = JSON.parse(text);
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const reasons =
|
|
184
|
+
gap &&
|
|
185
|
+
typeof gap.schema === 'string' &&
|
|
186
|
+
Object.hasOwn(GAP_SCHEMAS, gap.schema)
|
|
187
|
+
? GAP_SCHEMAS[gap.schema]
|
|
188
|
+
: null;
|
|
189
|
+
const missing =
|
|
190
|
+
gap && gap.missingCapabilities === undefined
|
|
191
|
+
? []
|
|
192
|
+
: gap && gap.missingCapabilities;
|
|
193
|
+
if (
|
|
194
|
+
!reasons ||
|
|
195
|
+
!reasons.includes(gap.reason) ||
|
|
196
|
+
gap.complete !== false ||
|
|
197
|
+
gap.verified !== false ||
|
|
198
|
+
typeof gap.target !== 'string' ||
|
|
199
|
+
!gap.target.trim() ||
|
|
200
|
+
!Array.isArray(missing) ||
|
|
201
|
+
!missing.every((c) => CAPABILITIES.has(c))
|
|
202
|
+
) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (echoedByTool(text, messages)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
schema: gap.schema,
|
|
210
|
+
target: gap.target,
|
|
211
|
+
reason: gap.reason,
|
|
212
|
+
missingCapabilities: missing,
|
|
213
|
+
complete: false,
|
|
214
|
+
verified: false,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
35
218
|
/**
|
|
36
219
|
* Normalize one chat completion into what the loop needs.
|
|
37
220
|
* @param {object} body - Parsed JSON response
|
|
@@ -52,17 +235,32 @@ function parseCompletion(body) {
|
|
|
52
235
|
if (!TOOL_NAMES.has(name)) {
|
|
53
236
|
throw new ProtocolError(`Formal AI called an unsupported tool "${name}"`);
|
|
54
237
|
}
|
|
238
|
+
const raw = call.function.arguments || '{}';
|
|
55
239
|
let args;
|
|
56
240
|
try {
|
|
57
|
-
args = JSON.parse(
|
|
241
|
+
args = JSON.parse(raw);
|
|
58
242
|
} catch {
|
|
59
243
|
throw new ProtocolError(`Formal AI sent non-JSON arguments for ${name}`);
|
|
60
244
|
}
|
|
61
|
-
|
|
245
|
+
if (!BASH_NAMES.has(name)) {
|
|
246
|
+
const normalized = normalizeFileArgs(name, args);
|
|
247
|
+
if (normalized.problem) {
|
|
248
|
+
throw new ProtocolError(`Formal AI called ${normalized.problem}`);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
id: call.id || null,
|
|
252
|
+
name,
|
|
253
|
+
tool: name,
|
|
254
|
+
args: normalized.args,
|
|
255
|
+
arguments: raw,
|
|
256
|
+
command: describeFileCall(name, normalized.args),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const command = args && (args.command ?? args.cmd);
|
|
62
260
|
if (typeof command !== 'string' || !command.trim()) {
|
|
63
261
|
throw new ProtocolError(`Formal AI called ${name} without a command`);
|
|
64
262
|
}
|
|
65
|
-
return { id: call.id || null, name, command };
|
|
263
|
+
return { id: call.id || null, name, tool: 'bash', command };
|
|
66
264
|
});
|
|
67
265
|
return {
|
|
68
266
|
content: typeof message.content === 'string' ? message.content : '',
|
|
@@ -74,14 +272,22 @@ function parseCompletion(body) {
|
|
|
74
272
|
|
|
75
273
|
/**
|
|
76
274
|
* Create a client bound to one runtime.
|
|
77
|
-
* @param {object} options - {baseUrl, bounds, deadline, token}
|
|
275
|
+
* @param {object} options - {baseUrl, bounds, deadline, token, tools}
|
|
78
276
|
* @returns {{complete: Function, requests: number}}
|
|
79
277
|
*/
|
|
80
|
-
function createClient({
|
|
278
|
+
function createClient({
|
|
279
|
+
baseUrl,
|
|
280
|
+
bounds,
|
|
281
|
+
deadline,
|
|
282
|
+
token = null,
|
|
283
|
+
tools = offeredTools(bounds.effects),
|
|
284
|
+
}) {
|
|
285
|
+
const secrets = token ? [token] : [];
|
|
81
286
|
const client = {
|
|
82
287
|
requests: 0,
|
|
83
288
|
async complete(messages, { signal } = {}) {
|
|
84
289
|
let lastError = null;
|
|
290
|
+
let attempts = 0;
|
|
85
291
|
for (let attempt = 0; attempt <= bounds.maxRetries; attempt++) {
|
|
86
292
|
const remaining = deadline - Date.now();
|
|
87
293
|
if (remaining <= 0) {
|
|
@@ -91,6 +297,7 @@ function createClient({ baseUrl, bounds, deadline, token = null }) {
|
|
|
91
297
|
);
|
|
92
298
|
}
|
|
93
299
|
client.requests++;
|
|
300
|
+
attempts++;
|
|
94
301
|
try {
|
|
95
302
|
const response = await fetch(
|
|
96
303
|
`${baseUrl}${API_PREFIX}/chat/completions`,
|
|
@@ -103,7 +310,7 @@ function createClient({ baseUrl, bounds, deadline, token = null }) {
|
|
|
103
310
|
body: JSON.stringify({
|
|
104
311
|
model: 'formal-ai',
|
|
105
312
|
messages,
|
|
106
|
-
tools
|
|
313
|
+
tools,
|
|
107
314
|
}),
|
|
108
315
|
signal: signal
|
|
109
316
|
? AbortSignal.any([signal, AbortSignal.timeout(remaining)])
|
|
@@ -111,16 +318,23 @@ function createClient({ baseUrl, bounds, deadline, token = null }) {
|
|
|
111
318
|
}
|
|
112
319
|
);
|
|
113
320
|
const text = await response.text();
|
|
114
|
-
if (response.status >= 500) {
|
|
115
|
-
lastError = new ProtocolError(
|
|
116
|
-
`Formal AI returned HTTP ${response.status}`
|
|
117
|
-
);
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
321
|
if (!response.ok) {
|
|
121
|
-
|
|
122
|
-
|
|
322
|
+
const details = {
|
|
323
|
+
...httpErrorDetails(response.status, text, secrets),
|
|
324
|
+
attempts,
|
|
325
|
+
};
|
|
326
|
+
const error = new ProtocolError(
|
|
327
|
+
httpErrorMessage(details),
|
|
328
|
+
EXIT.PROTOCOL,
|
|
329
|
+
details
|
|
123
330
|
);
|
|
331
|
+
// A server error may be transient: retry it within the bounds,
|
|
332
|
+
// keeping the latest diagnostics.
|
|
333
|
+
if (response.status >= 500) {
|
|
334
|
+
lastError = error;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
124
338
|
}
|
|
125
339
|
let body;
|
|
126
340
|
try {
|
|
@@ -142,15 +356,23 @@ function createClient({ baseUrl, bounds, deadline, token = null }) {
|
|
|
142
356
|
EXIT.BUDGET
|
|
143
357
|
);
|
|
144
358
|
}
|
|
359
|
+
const cause = sanitizeDiagnostic(
|
|
360
|
+
error.message,
|
|
361
|
+
MESSAGE_CHARS,
|
|
362
|
+
secrets
|
|
363
|
+
);
|
|
145
364
|
lastError = new ProtocolError(
|
|
146
|
-
`Formal AI request failed: ${
|
|
147
|
-
EXIT.UNAVAILABLE
|
|
365
|
+
`Formal AI request failed: ${cause}`,
|
|
366
|
+
EXIT.UNAVAILABLE,
|
|
367
|
+
{ cause, attempts }
|
|
148
368
|
);
|
|
149
369
|
}
|
|
150
370
|
}
|
|
151
|
-
|
|
152
|
-
|
|
371
|
+
const retries = attempts - 1;
|
|
372
|
+
if (retries > 0) {
|
|
373
|
+
lastError.message += ` (after ${retries} retr${retries === 1 ? 'y' : 'ies'})`;
|
|
153
374
|
}
|
|
375
|
+
lastError.details.attempts = attempts;
|
|
154
376
|
throw lastError;
|
|
155
377
|
},
|
|
156
378
|
};
|
|
@@ -159,8 +381,13 @@ function createClient({ baseUrl, bounds, deadline, token = null }) {
|
|
|
159
381
|
|
|
160
382
|
module.exports = {
|
|
161
383
|
BASH_TOOL,
|
|
384
|
+
GAP_SCHEMAS,
|
|
162
385
|
ProtocolError,
|
|
163
386
|
TOOL_NAMES,
|
|
164
387
|
createClient,
|
|
388
|
+
httpErrorDetails,
|
|
389
|
+
offeredTools,
|
|
165
390
|
parseCompletion,
|
|
391
|
+
recognizeCapabilityGap,
|
|
392
|
+
sanitizeDiagnostic,
|
|
166
393
|
};
|