@the-open-engine/zeroshot 6.16.0 → 6.18.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 +2 -2
- package/cli/index.js +9 -13
- package/lib/agent-cli-provider/adapters/claude.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +30 -0
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +37 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.js +115 -4
- package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
- package/lib/agent-cli-provider/adapters/omp.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/omp.js +356 -0
- package/lib/agent-cli-provider/adapters/omp.js.map +1 -0
- package/lib/agent-cli-provider/adapters/opencode.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +108 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +17 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +2 -2
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +65 -7
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +43 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +35 -4
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +49 -4
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster/errors.cjs +4 -1
- package/lib/cluster/errors.d.ts +2 -0
- package/lib/cluster/errors.mjs +2 -0
- package/lib/cluster/index.cjs +8 -1
- package/lib/cluster/index.d.ts +4 -1
- package/lib/cluster/index.mjs +4 -1
- package/lib/cluster/json-source.cjs +61 -0
- package/lib/cluster/json-source.d.ts +4 -0
- package/lib/cluster/json-source.mjs +55 -0
- package/lib/cluster/payload-value.cjs +76 -0
- package/lib/cluster/payload-value.d.ts +7 -0
- package/lib/cluster/payload-value.mjs +72 -0
- package/lib/cluster/validators.cjs +21 -0
- package/lib/cluster/validators.d.ts +4 -1
- package/lib/cluster/validators.mjs +19 -1
- package/lib/provider-names.js +5 -0
- package/package.json +2 -2
- package/src/agent/agent-task-executor.js +194 -410
- package/src/agent/output-extraction.js +39 -14
- package/src/agent/output-reformatter.js +154 -111
- package/src/agent-cli-provider/adapters/claude.ts +39 -2
- package/src/agent-cli-provider/adapters/codex.ts +56 -2
- package/src/agent-cli-provider/adapters/gemini.ts +102 -6
- package/src/agent-cli-provider/adapters/omp.ts +448 -0
- package/src/agent-cli-provider/adapters/opencode.ts +85 -2
- package/src/agent-cli-provider/contract-options.ts +21 -0
- package/src/agent-cli-provider/index.ts +5 -0
- package/src/agent-cli-provider/provider-registry.ts +62 -3
- package/src/agent-cli-provider/single-agent-runtime.ts +50 -4
- package/src/agent-cli-provider/types.ts +56 -2
- package/src/agent-wrapper.js +2 -2
- package/src/cluster/errors.ts +1 -0
- package/src/cluster/index.ts +4 -0
- package/src/cluster/json-source.ts +71 -0
- package/src/cluster/payload-value.ts +84 -0
- package/src/cluster/validators.ts +31 -2
- package/src/orchestrator.js +33 -13
- package/task-lib/attachable-watcher.js +1 -0
- package/task-lib/command-spec-cleanup.js +57 -11
- package/task-lib/commands/run.js +1 -0
- package/task-lib/provider-session-capture.js +8 -0
- package/task-lib/runner.js +9 -6
- package/task-lib/watcher.js +1 -0
|
@@ -140,6 +140,28 @@ function extractFromTextEvents(output, providerName) {
|
|
|
140
140
|
return null;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Assemble assistant text through the active provider parser. Recovery prompts
|
|
145
|
+
* use this instead of exposing raw event envelopes when model text exists.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} output
|
|
148
|
+
* @param {string} providerName
|
|
149
|
+
* @returns {string|null}
|
|
150
|
+
*/
|
|
151
|
+
function extractModelTextFromOutput(output, providerName) {
|
|
152
|
+
if (!output || typeof output !== 'string') return null;
|
|
153
|
+
const normalized = output
|
|
154
|
+
.split('\n')
|
|
155
|
+
.map((line) => stripTimestamp(line))
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join('\n');
|
|
158
|
+
const text = parseProviderChunk(providerName, normalized)
|
|
159
|
+
.filter((event) => event.type === 'text' && typeof event.text === 'string')
|
|
160
|
+
.map((event) => event.text)
|
|
161
|
+
.join('');
|
|
162
|
+
return text.trim() ? text : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
143
165
|
/**
|
|
144
166
|
* Strategy 3: Extract JSON from markdown code block
|
|
145
167
|
* Handles: ```json\n{...}\n```
|
|
@@ -280,13 +302,13 @@ function extractClaudeVertexModelError(output, { useVertex = false } = {}) {
|
|
|
280
302
|
* Provider error formats:
|
|
281
303
|
* - Claude: {type:"result", is_error:true, errors:["msg"]} or {type:"result", subtype:"error"}
|
|
282
304
|
* - Codex: {type:"turn.failed", error:{message:"msg"}}
|
|
283
|
-
* - Gemini: {type:"result",
|
|
305
|
+
* - Gemini: {type:"result", status:"error", error:{message:"msg"}} or {type:"error", severity:"error"}
|
|
284
306
|
* - Opencode: {type:"session.error", error:{message:"msg"}}
|
|
285
307
|
*
|
|
286
|
-
* @param {string}
|
|
308
|
+
* @param {string} providerName - Active provider whose terminal errors may be inspected
|
|
287
309
|
* @returns {{error: string, provider: string}|null} Error info or null
|
|
288
310
|
*/
|
|
289
|
-
function extractCliError(output) {
|
|
311
|
+
function extractCliError(output, providerName = 'claude') {
|
|
290
312
|
if (!output || typeof output !== 'string') return null;
|
|
291
313
|
|
|
292
314
|
const lines = output.split('\n');
|
|
@@ -302,33 +324,35 @@ function extractCliError(output) {
|
|
|
302
324
|
continue;
|
|
303
325
|
}
|
|
304
326
|
|
|
305
|
-
|
|
306
|
-
if (obj.type === 'result' && obj.is_error === true) {
|
|
327
|
+
if (providerName === 'claude' && obj.type === 'result' && obj.is_error === true) {
|
|
307
328
|
const errorMsg = Array.isArray(obj.errors)
|
|
308
329
|
? obj.errors.join('; ')
|
|
309
330
|
: obj.error || obj.result || 'Unknown CLI error';
|
|
310
331
|
return { error: errorMsg, provider: 'claude' };
|
|
311
332
|
}
|
|
312
333
|
|
|
313
|
-
|
|
314
|
-
if (obj.type === 'result' && obj.subtype === 'error') {
|
|
334
|
+
if (providerName === 'claude' && obj.type === 'result' && obj.subtype === 'error') {
|
|
315
335
|
const errorMsg = obj.error || obj.result || 'CLI returned error';
|
|
316
336
|
return { error: errorMsg, provider: 'claude' };
|
|
317
337
|
}
|
|
318
338
|
|
|
319
|
-
|
|
320
|
-
if (obj.type === 'turn.failed') {
|
|
339
|
+
if (providerName === 'codex' && obj.type === 'turn.failed') {
|
|
321
340
|
const errorMsg = obj.error?.message || obj.error || 'Turn failed';
|
|
322
341
|
return { error: errorMsg, provider: 'codex' };
|
|
323
342
|
}
|
|
324
343
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
344
|
+
if (
|
|
345
|
+
providerName === 'gemini' &&
|
|
346
|
+
((obj.type === 'result' && obj.status === 'error') ||
|
|
347
|
+
(obj.type === 'error' && obj.severity === 'error'))
|
|
348
|
+
) {
|
|
349
|
+
return {
|
|
350
|
+
error: obj.error?.message || obj.message || 'Gemini CLI error',
|
|
351
|
+
provider: 'gemini',
|
|
352
|
+
};
|
|
328
353
|
}
|
|
329
354
|
|
|
330
|
-
|
|
331
|
-
if (obj.type === 'session.error') {
|
|
355
|
+
if (providerName === 'opencode' && (obj.type === 'session.error' || obj.type === 'error')) {
|
|
332
356
|
const errorMsg =
|
|
333
357
|
obj.error?.data?.message || obj.error?.message || obj.error?.name || 'Session error';
|
|
334
358
|
return { error: errorMsg, provider: 'opencode' };
|
|
@@ -396,6 +420,7 @@ function extractJsonFromOutput(output, providerName = 'claude') {
|
|
|
396
420
|
|
|
397
421
|
module.exports = {
|
|
398
422
|
extractJsonFromOutput,
|
|
423
|
+
extractModelTextFromOutput,
|
|
399
424
|
extractCliError,
|
|
400
425
|
extractClaudeVertexModelError,
|
|
401
426
|
extractFromResultWrapper,
|
|
@@ -1,49 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* When an LLM outputs markdown/text instead of JSON despite schema instructions,
|
|
5
|
-
* this module attempts to extract/reformat the content into valid JSON.
|
|
6
|
-
*
|
|
7
|
-
* Opencode output can be reformatted through its CLI when direct JSON extraction fails.
|
|
8
|
-
* Other providers remain on their own extraction paths and never depend on the opencode binary.
|
|
9
|
-
*/
|
|
1
|
+
const Ajv = require('ajv');
|
|
2
|
+
const { getProvider } = require('../providers');
|
|
3
|
+
const { normalizeEnumValues } = require('./schema-utils');
|
|
10
4
|
|
|
11
5
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
12
|
-
|
|
6
|
+
const MAX_REFORMAT_INPUT_BYTES = 65_536;
|
|
7
|
+
const MAX_CONFIGURED_ATTEMPTS = 10;
|
|
13
8
|
|
|
14
9
|
function createCancellationError() {
|
|
15
10
|
const error = new Error('Output reformatting cancelled');
|
|
16
11
|
error.code = 'REFORMAT_CANCELLED';
|
|
12
|
+
error.recoveryAbort = true;
|
|
17
13
|
return error;
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Build the reformatting prompt
|
|
23
|
-
*
|
|
24
|
-
* @param {string} rawOutput - The non-JSON output to reformat
|
|
25
|
-
* @param {Object} schema - Target JSON schema
|
|
26
|
-
* @param {string|null} previousError - Error from previous attempt (for feedback)
|
|
27
|
-
* @returns {string} The prompt for the reformatting model
|
|
28
|
-
*/
|
|
29
16
|
function buildReformatPrompt(rawOutput, schema, previousError = null) {
|
|
30
|
-
const schemaStr = JSON.stringify(schema, null, 2);
|
|
31
|
-
// Truncate long outputs to avoid context limits
|
|
32
|
-
const truncatedOutput = rawOutput.length > 4000 ? rawOutput.slice(-4000) : rawOutput;
|
|
33
|
-
|
|
34
17
|
let prompt = `CRITICAL: Do NOT use any tools. Do NOT read, write, or edit any files. Do NOT explore the codebase. This is a pure text-to-JSON transformation — respond with JSON only.
|
|
35
18
|
|
|
36
|
-
Convert
|
|
19
|
+
Convert the JSON-encoded source text into a JSON object matching the schema.
|
|
37
20
|
|
|
38
21
|
## SCHEMA
|
|
39
22
|
\`\`\`json
|
|
40
|
-
${
|
|
23
|
+
${JSON.stringify(schema, null, 2)}
|
|
41
24
|
\`\`\`
|
|
42
25
|
|
|
43
|
-
##
|
|
44
|
-
|
|
45
|
-
${truncatedOutput}
|
|
46
|
-
\`\`\`
|
|
26
|
+
## JSON-ENCODED SOURCE TEXT
|
|
27
|
+
${JSON.stringify(rawOutput)}
|
|
47
28
|
|
|
48
29
|
## RULES
|
|
49
30
|
- Output ONLY the JSON object
|
|
@@ -55,134 +36,196 @@ ${truncatedOutput}
|
|
|
55
36
|
if (previousError) {
|
|
56
37
|
prompt += `
|
|
57
38
|
|
|
58
|
-
## PREVIOUS
|
|
59
|
-
|
|
39
|
+
## PREVIOUS CANDIDATE FAILED
|
|
40
|
+
${previousError}
|
|
60
41
|
Fix this issue in your response.`;
|
|
61
42
|
}
|
|
62
43
|
|
|
63
44
|
return prompt;
|
|
64
45
|
}
|
|
65
46
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
47
|
+
function formatValidationErrors(errors, limit = 5) {
|
|
48
|
+
return errors
|
|
49
|
+
.slice(0, limit)
|
|
50
|
+
.map((error) => `${error.instancePath || error.schemaPath || '#'} ${error.message}`)
|
|
51
|
+
.join('; ');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createStructuredOutputValidator(schema) {
|
|
55
|
+
const ajv = new Ajv({
|
|
56
|
+
allErrors: true,
|
|
57
|
+
strict: false,
|
|
58
|
+
coerceTypes: false,
|
|
59
|
+
useDefaults: true,
|
|
60
|
+
removeAdditional: true,
|
|
61
|
+
});
|
|
62
|
+
const validate = ajv.compile(schema);
|
|
63
|
+
|
|
64
|
+
return (candidate) => {
|
|
65
|
+
normalizeEnumValues(candidate, schema);
|
|
66
|
+
const valid = validate(candidate);
|
|
67
|
+
const errors = (validate.errors || []).map((error) => ({ ...error }));
|
|
68
|
+
return {
|
|
69
|
+
valid,
|
|
70
|
+
value: candidate,
|
|
71
|
+
errors,
|
|
72
|
+
error: valid ? null : formatValidationErrors(errors) || 'Schema validation failed',
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateAgainstSchema(parsed, schema) {
|
|
78
|
+
return createStructuredOutputValidator(schema)(parsed).error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertReformatRequest(rawOutput, maxAttempts) {
|
|
82
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > MAX_CONFIGURED_ATTEMPTS) {
|
|
83
|
+
const error = new RangeError(
|
|
84
|
+
`Output reformatting maxAttempts must be an integer from 1 through ${MAX_CONFIGURED_ATTEMPTS}`
|
|
85
|
+
);
|
|
86
|
+
error.code = 'REFORMAT_INVALID_ATTEMPT_LIMIT';
|
|
87
|
+
error.permanent = true;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (Buffer.byteLength(rawOutput || '', 'utf8') > MAX_REFORMAT_INPUT_BYTES) {
|
|
91
|
+
const error = new Error(
|
|
92
|
+
`Output reformatting input exceeds ${MAX_REFORMAT_INPUT_BYTES} UTF-8 bytes`
|
|
93
|
+
);
|
|
94
|
+
error.code = 'REFORMAT_INPUT_TOO_LARGE';
|
|
95
|
+
error.permanent = true;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function invocationError(result) {
|
|
101
|
+
const error =
|
|
102
|
+
result?.error instanceof Error
|
|
103
|
+
? result.error
|
|
104
|
+
: new Error(result?.error || 'Structured-output recovery task failed');
|
|
105
|
+
for (const field of [
|
|
106
|
+
'code',
|
|
107
|
+
'permanent',
|
|
108
|
+
'provider',
|
|
109
|
+
'capability',
|
|
110
|
+
'nestedExecutionCancellation',
|
|
111
|
+
'nestedExecutionLifecycle',
|
|
112
|
+
'retainTaskHandle',
|
|
113
|
+
'terminationExhausted',
|
|
114
|
+
'taskId',
|
|
115
|
+
]) {
|
|
116
|
+
if (result?.[field] !== undefined && error[field] === undefined) {
|
|
117
|
+
error[field] = result[field];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isImmediateRecoveryFailure(error, providerName) {
|
|
124
|
+
if (
|
|
125
|
+
error?.code === 'REFORMAT_CANCELLED' ||
|
|
126
|
+
error?.code === 'AGENT_TASK_TIMEOUT' ||
|
|
127
|
+
error?.nestedExecutionCancellation === true ||
|
|
128
|
+
error?.nestedExecutionLifecycle === true ||
|
|
129
|
+
error?.retainTaskHandle === true ||
|
|
130
|
+
error?.permanent === true ||
|
|
131
|
+
error?.terminationExhausted === true
|
|
132
|
+
) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return !getProvider(providerName).isRetryableError(error);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function markImmediateRecoveryFailure(error, providerName) {
|
|
139
|
+
if (!isImmediateRecoveryFailure(error, providerName)) return false;
|
|
140
|
+
error.recoveryAbort = true;
|
|
141
|
+
const operationalControl =
|
|
142
|
+
error.code === 'REFORMAT_CANCELLED' ||
|
|
143
|
+
error.code === 'AGENT_TASK_TIMEOUT' ||
|
|
144
|
+
error.nestedExecutionCancellation === true ||
|
|
145
|
+
error.nestedExecutionLifecycle === true ||
|
|
146
|
+
error.retainTaskHandle === true ||
|
|
147
|
+
error.terminationExhausted === true;
|
|
148
|
+
if (!operationalControl && error.permanent !== true) error.permanent = true;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
81
152
|
async function reformatOutput({
|
|
82
153
|
rawOutput,
|
|
83
154
|
schema,
|
|
84
155
|
providerName,
|
|
85
156
|
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
157
|
+
initialError = null,
|
|
158
|
+
validateCandidate = createStructuredOutputValidator(schema),
|
|
86
159
|
onAttempt,
|
|
87
160
|
isCancelled = () => false,
|
|
88
161
|
runReformat,
|
|
89
162
|
}) {
|
|
90
|
-
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Output reformatting not available for provider "${providerName}". ` +
|
|
93
|
-
`Agent output must be valid JSON. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
|
|
94
|
-
);
|
|
95
|
-
}
|
|
163
|
+
assertReformatRequest(rawOutput, maxAttempts);
|
|
96
164
|
if (typeof runReformat !== 'function') {
|
|
97
165
|
throw new Error('Output reformatting requires the active agent execution context');
|
|
98
166
|
}
|
|
99
167
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
let lastError = null;
|
|
168
|
+
const { extractCliError, extractJsonFromOutput } = require('./output-extraction');
|
|
169
|
+
let lastError = initialError;
|
|
103
170
|
|
|
104
171
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
105
172
|
if (isCancelled()) throw createCancellationError();
|
|
106
|
-
|
|
107
|
-
onAttempt(attempt, lastError);
|
|
108
|
-
}
|
|
109
|
-
|
|
173
|
+
onAttempt?.(attempt, lastError);
|
|
110
174
|
const prompt = buildReformatPrompt(rawOutput, schema, lastError);
|
|
111
175
|
|
|
112
176
|
try {
|
|
113
177
|
const result = await runReformat(prompt);
|
|
114
178
|
if (isCancelled()) throw createCancellationError();
|
|
115
179
|
if (!result?.success) {
|
|
116
|
-
|
|
180
|
+
const error = invocationError(result);
|
|
181
|
+
if (markImmediateRecoveryFailure(error, providerName)) throw error;
|
|
182
|
+
lastError = error.message;
|
|
117
183
|
continue;
|
|
118
184
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
lastError = 'reformat task returned no output';
|
|
185
|
+
if (!result.output) {
|
|
186
|
+
lastError = 'Recovery task returned no output';
|
|
122
187
|
continue;
|
|
123
188
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
189
|
+
const terminalError = extractCliError(result.output, providerName);
|
|
190
|
+
if (terminalError) {
|
|
191
|
+
const error = new Error(terminalError.error);
|
|
192
|
+
error.provider = terminalError.provider;
|
|
193
|
+
if (markImmediateRecoveryFailure(error, providerName)) throw error;
|
|
194
|
+
lastError = error.message;
|
|
128
195
|
continue;
|
|
129
196
|
}
|
|
130
197
|
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
lastError =
|
|
198
|
+
const parsed = extractJsonFromOutput(result.output, providerName);
|
|
199
|
+
if (!parsed) {
|
|
200
|
+
lastError = 'Could not extract JSON from recovery output';
|
|
134
201
|
continue;
|
|
135
202
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
err.code === 'REFORMAT_CANCELLED' ||
|
|
141
|
-
err.code === 'AGENT_TASK_TIMEOUT' ||
|
|
142
|
-
err.nestedExecutionLifecycle === true ||
|
|
143
|
-
err.retainTaskHandle === true ||
|
|
144
|
-
err.permanent === true ||
|
|
145
|
-
err.terminationExhausted === true
|
|
146
|
-
) {
|
|
147
|
-
throw err;
|
|
203
|
+
const validation = validateCandidate(parsed);
|
|
204
|
+
if (!validation.valid) {
|
|
205
|
+
lastError = validation.error;
|
|
206
|
+
continue;
|
|
148
207
|
}
|
|
149
|
-
|
|
208
|
+
return { status: 'recovered', value: validation.value, attempts: attempt };
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (isCancelled()) throw createCancellationError();
|
|
211
|
+
if (markImmediateRecoveryFailure(error, providerName)) throw error;
|
|
212
|
+
lastError = error.message;
|
|
150
213
|
}
|
|
151
214
|
}
|
|
152
215
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Validate parsed output against JSON schema
|
|
161
|
-
*
|
|
162
|
-
* @param {Object} parsed - Parsed JSON object
|
|
163
|
-
* @param {Object} schema - JSON schema to validate against
|
|
164
|
-
* @returns {string|null} Error message if validation failed, null if valid
|
|
165
|
-
*/
|
|
166
|
-
function validateAgainstSchema(parsed, schema) {
|
|
167
|
-
const Ajv = require('ajv');
|
|
168
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
169
|
-
const validate = ajv.compile(schema);
|
|
170
|
-
const valid = validate(parsed);
|
|
171
|
-
|
|
172
|
-
if (!valid) {
|
|
173
|
-
const errors = (validate.errors || [])
|
|
174
|
-
.slice(0, 3)
|
|
175
|
-
.map((e) => `${e.instancePath || '#'} ${e.message}`)
|
|
176
|
-
.join('; ');
|
|
177
|
-
return errors || 'Schema validation failed';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return null;
|
|
216
|
+
return {
|
|
217
|
+
status: 'exhausted',
|
|
218
|
+
attempts: maxAttempts,
|
|
219
|
+
lastError: lastError || 'Recovery attempts produced no schema-valid JSON object',
|
|
220
|
+
};
|
|
181
221
|
}
|
|
182
222
|
|
|
183
223
|
module.exports = {
|
|
184
224
|
reformatOutput,
|
|
185
225
|
buildReformatPrompt,
|
|
226
|
+
createStructuredOutputValidator,
|
|
186
227
|
validateAgainstSchema,
|
|
187
228
|
DEFAULT_MAX_ATTEMPTS,
|
|
229
|
+
MAX_REFORMAT_INPUT_BYTES,
|
|
230
|
+
MAX_CONFIGURED_ATTEMPTS,
|
|
188
231
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
|
|
2
|
+
import { UnsupportedProviderCapabilityError } from '../errors';
|
|
2
3
|
import { contractError } from '../contract-errors';
|
|
3
4
|
import {
|
|
4
5
|
type BuildProviderCommandOptions,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
type LevelOverrides,
|
|
10
11
|
type ModelCatalogEntry,
|
|
11
12
|
type ModelLevel,
|
|
12
|
-
type
|
|
13
|
+
type StructuredOutputRecoveryAdapter,
|
|
13
14
|
type ResolvedModelSpec,
|
|
14
15
|
type WarningMetadata,
|
|
15
16
|
} from '../types';
|
|
@@ -72,6 +73,9 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
|
|
|
72
73
|
supportsSettings: !unknown && /--settings/.test(help),
|
|
73
74
|
supportsMcpConfig: !unknown && /--mcp-config/.test(help),
|
|
74
75
|
supportsResume: unknown ? true : /--resume/.test(help),
|
|
76
|
+
supportsTools: !unknown && /--tools\b/.test(help),
|
|
77
|
+
supportsStrictMcpConfig: !unknown && /--strict-mcp-config\b/.test(help),
|
|
78
|
+
supportsNoSessionPersistence: !unknown && /--no-session-persistence\b/.test(help),
|
|
75
79
|
unknown,
|
|
76
80
|
};
|
|
77
81
|
}
|
|
@@ -252,6 +256,38 @@ function buildCommand(context: string, options: BuildProviderCommandOptions = {}
|
|
|
252
256
|
});
|
|
253
257
|
}
|
|
254
258
|
|
|
259
|
+
function assertStructuredOutputRecoveryFeatures(options: BuildProviderCommandOptions): void {
|
|
260
|
+
const features = optionFeatures(options);
|
|
261
|
+
const missing = [
|
|
262
|
+
['--tools', features.supportsTools],
|
|
263
|
+
['--strict-mcp-config', features.supportsStrictMcpConfig],
|
|
264
|
+
['--no-session-persistence', features.supportsNoSessionPersistence],
|
|
265
|
+
].filter(([, supported]) => supported !== true);
|
|
266
|
+
if (missing.length === 0) return;
|
|
267
|
+
throw new UnsupportedProviderCapabilityError(
|
|
268
|
+
'claude',
|
|
269
|
+
'structuredOutputRecovery',
|
|
270
|
+
`Claude structured-output recovery requires CLI evidence for ${missing.map(([flag]) => flag).join(', ')}. Upgrade Claude Code before retrying.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildStructuredOutputRecoveryCommand(
|
|
275
|
+
context: string,
|
|
276
|
+
options: BuildProviderCommandOptions = {}
|
|
277
|
+
): CommandSpec {
|
|
278
|
+
assertStructuredOutputRecoveryFeatures(options);
|
|
279
|
+
const recoveryOptions = { ...options };
|
|
280
|
+
delete recoveryOptions.resumeSessionId;
|
|
281
|
+
delete recoveryOptions.continueSession;
|
|
282
|
+
delete recoveryOptions.mcpConfig;
|
|
283
|
+
const spec = buildCommand(context, { ...recoveryOptions, autoApprove: false });
|
|
284
|
+
const args = [...spec.args];
|
|
285
|
+
const prompt = args.pop();
|
|
286
|
+
if (prompt === undefined) throw new Error('Claude recovery command is missing its prompt');
|
|
287
|
+
args.push('--tools', '', '--strict-mcp-config', '--no-session-persistence', prompt);
|
|
288
|
+
return { ...spec, args };
|
|
289
|
+
}
|
|
290
|
+
|
|
255
291
|
function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
|
|
256
292
|
return resolveModelSpecWithConfig({
|
|
257
293
|
mapping: LEVEL_MAPPING,
|
|
@@ -274,7 +310,7 @@ function classifyError(error: unknown): ErrorClassification {
|
|
|
274
310
|
);
|
|
275
311
|
}
|
|
276
312
|
|
|
277
|
-
export const claudeAdapter:
|
|
313
|
+
export const claudeAdapter: StructuredOutputRecoveryAdapter = {
|
|
278
314
|
id: 'claude',
|
|
279
315
|
displayName: 'Claude',
|
|
280
316
|
binary: 'claude',
|
|
@@ -287,6 +323,7 @@ export const claudeAdapter: ProviderAdapter = {
|
|
|
287
323
|
defaultMinLevel: 'level1',
|
|
288
324
|
detectCliFeatures,
|
|
289
325
|
buildCommand,
|
|
326
|
+
buildStructuredOutputRecoveryCommand,
|
|
290
327
|
extractSessionId,
|
|
291
328
|
parseEvent: parseClaudeEvent,
|
|
292
329
|
createParserState: () => createParserState('claude'),
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type LevelOverrides,
|
|
11
11
|
type ModelCatalogEntry,
|
|
12
12
|
type ModelLevel,
|
|
13
|
-
type
|
|
13
|
+
type StructuredOutputRecoveryAdapter,
|
|
14
14
|
type ResolvedModelSpec,
|
|
15
15
|
type WarningMetadata,
|
|
16
16
|
} from '../types';
|
|
@@ -67,6 +67,11 @@ function detectCliFeatures(
|
|
|
67
67
|
supportsResume: supports(help, /\bresume\b/),
|
|
68
68
|
supportsWebSearch:
|
|
69
69
|
!unknown && supportsConfigOverride && isCliVersionAtLeast(versionText, '0.146.0'),
|
|
70
|
+
supportsSandbox: !unknown && /--sandbox\b/.test(help),
|
|
71
|
+
supportsEphemeral: !unknown && /--ephemeral\b/.test(help),
|
|
72
|
+
supportsIgnoreUserConfig: !unknown && /--ignore-user-config\b/.test(help),
|
|
73
|
+
supportsIgnoreRules: !unknown && /--ignore-rules\b/.test(help),
|
|
74
|
+
supportsStrictConfig: !unknown && /--strict-config\b/.test(help),
|
|
70
75
|
unknown,
|
|
71
76
|
};
|
|
72
77
|
}
|
|
@@ -245,6 +250,54 @@ function buildCommand(context: string, options: BuildProviderCommandOptions = {}
|
|
|
245
250
|
});
|
|
246
251
|
}
|
|
247
252
|
|
|
253
|
+
function assertStructuredOutputRecoveryFeatures(options: BuildProviderCommandOptions): void {
|
|
254
|
+
const features = optionFeatures(options);
|
|
255
|
+
const missing = [
|
|
256
|
+
['--sandbox', features.supportsSandbox],
|
|
257
|
+
['--ephemeral', features.supportsEphemeral],
|
|
258
|
+
['--ignore-user-config', features.supportsIgnoreUserConfig],
|
|
259
|
+
['--ignore-rules', features.supportsIgnoreRules],
|
|
260
|
+
['--strict-config', features.supportsStrictConfig],
|
|
261
|
+
['--config', features.supportsConfigOverride],
|
|
262
|
+
].filter(([, supported]) => supported !== true);
|
|
263
|
+
if (missing.length === 0) return;
|
|
264
|
+
throw new UnsupportedProviderCapabilityError(
|
|
265
|
+
'codex',
|
|
266
|
+
'structuredOutputRecovery',
|
|
267
|
+
`Codex structured-output recovery requires CLI evidence for ${missing.map(([flag]) => flag).join(', ')}. Upgrade @openai/codex before retrying.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildStructuredOutputRecoveryCommand(
|
|
272
|
+
context: string,
|
|
273
|
+
options: BuildProviderCommandOptions = {}
|
|
274
|
+
): CommandSpec {
|
|
275
|
+
assertStructuredOutputRecoveryFeatures(options);
|
|
276
|
+
const recoveryOptions = { ...options };
|
|
277
|
+
delete recoveryOptions.resumeSessionId;
|
|
278
|
+
delete recoveryOptions.continueSession;
|
|
279
|
+
const spec = buildCommand(context, {
|
|
280
|
+
...recoveryOptions,
|
|
281
|
+
autoApprove: false,
|
|
282
|
+
webSearch: false,
|
|
283
|
+
});
|
|
284
|
+
const args = [...spec.args];
|
|
285
|
+
const prompt = args.pop();
|
|
286
|
+
if (prompt === undefined) throw new Error('Codex recovery command is missing its prompt');
|
|
287
|
+
args.push(
|
|
288
|
+
'--sandbox',
|
|
289
|
+
'read-only',
|
|
290
|
+
'--ephemeral',
|
|
291
|
+
'--ignore-user-config',
|
|
292
|
+
'--ignore-rules',
|
|
293
|
+
'--strict-config',
|
|
294
|
+
'--config',
|
|
295
|
+
'web_search="disabled"',
|
|
296
|
+
prompt
|
|
297
|
+
);
|
|
298
|
+
return { ...spec, args };
|
|
299
|
+
}
|
|
300
|
+
|
|
248
301
|
function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
|
|
249
302
|
return resolveModelSpecWithConfig({
|
|
250
303
|
mapping: LEVEL_MAPPING,
|
|
@@ -267,7 +320,7 @@ function classifyError(error: unknown): ErrorClassification {
|
|
|
267
320
|
);
|
|
268
321
|
}
|
|
269
322
|
|
|
270
|
-
export const codexAdapter:
|
|
323
|
+
export const codexAdapter: StructuredOutputRecoveryAdapter = {
|
|
271
324
|
id: 'codex',
|
|
272
325
|
displayName: 'Codex',
|
|
273
326
|
binary: 'codex',
|
|
@@ -280,6 +333,7 @@ export const codexAdapter: ProviderAdapter = {
|
|
|
280
333
|
defaultMinLevel: 'level1',
|
|
281
334
|
detectCliFeatures,
|
|
282
335
|
buildCommand,
|
|
336
|
+
buildStructuredOutputRecoveryCommand,
|
|
283
337
|
extractSessionId,
|
|
284
338
|
parseEvent: parseCodexEvent,
|
|
285
339
|
createParserState: () => createParserState('codex'),
|