@the-open-engine/zeroshot 6.34.1 → 6.34.3
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/cli/index.js +34 -8
- package/cli/json-export.js +77 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +24 -19
- package/src/agent/agent-task-executor.js +539 -157
- package/src/agent/output-extraction.js +111 -39
- package/src/agent/provider-terminal-failure.js +186 -0
- package/src/ledger.js +364 -31
- package/src/orchestrator.js +33 -1
- package/src/template-resolver.js +5 -0
- package/task-lib/watcher-output-runtime.js +128 -43
|
@@ -16,8 +16,51 @@
|
|
|
16
16
|
* 4. Direct JSON parse of entire output
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
const { createHash } = require('node:crypto');
|
|
19
20
|
const { parseProviderChunk } = require('../providers');
|
|
20
21
|
|
|
22
|
+
const MAX_CLI_ERROR_BYTES = 4096;
|
|
23
|
+
const CLI_ERROR_TRUNCATION_SUFFIX = '… [truncated]';
|
|
24
|
+
|
|
25
|
+
function truncateUtf8(text, maxBytes = MAX_CLI_ERROR_BYTES) {
|
|
26
|
+
if (Buffer.byteLength(text) <= maxBytes) return text;
|
|
27
|
+
|
|
28
|
+
const suffixBytes = Buffer.byteLength(CLI_ERROR_TRUNCATION_SUFFIX);
|
|
29
|
+
const contentBudget = Math.max(0, maxBytes - suffixBytes);
|
|
30
|
+
let bytes = 0;
|
|
31
|
+
let truncated = '';
|
|
32
|
+
for (const character of text) {
|
|
33
|
+
const characterBytes = Buffer.byteLength(character);
|
|
34
|
+
if (bytes + characterBytes > contentBudget) break;
|
|
35
|
+
truncated += character;
|
|
36
|
+
bytes += characterBytes;
|
|
37
|
+
}
|
|
38
|
+
return `${truncated}${CLI_ERROR_TRUNCATION_SUFFIX}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function primitiveErrorText(value) {
|
|
42
|
+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
43
|
+
? String(value)
|
|
44
|
+
: '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cliErrorDetail(value, fallback) {
|
|
48
|
+
const candidate = Array.isArray(value)
|
|
49
|
+
? value
|
|
50
|
+
.map(primitiveErrorText)
|
|
51
|
+
.filter((message) => message.trim())
|
|
52
|
+
.join('; ')
|
|
53
|
+
: primitiveErrorText(value);
|
|
54
|
+
const raw = candidate.trim() ? candidate : fallback;
|
|
55
|
+
return {
|
|
56
|
+
error: truncateUtf8(raw.trim()),
|
|
57
|
+
diagnostic: {
|
|
58
|
+
byteLength: Buffer.byteLength(raw),
|
|
59
|
+
sha256: createHash('sha256').update(raw).digest('hex'),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
/**
|
|
22
65
|
* Strip timestamp prefix from log lines.
|
|
23
66
|
* Format: [epochMs]content or [epochMs]{json...}
|
|
@@ -308,60 +351,87 @@ function extractClaudeVertexModelError(output, { useVertex = false } = {}) {
|
|
|
308
351
|
* @param {string} providerName - Active provider whose terminal errors may be inspected
|
|
309
352
|
* @returns {{error: string, provider: string}|null} Error info or null
|
|
310
353
|
*/
|
|
311
|
-
function
|
|
354
|
+
function claudeFailureFromObject(obj) {
|
|
355
|
+
if (obj.type !== 'result') return null;
|
|
356
|
+
if (obj.is_error === true) {
|
|
357
|
+
const detail = cliErrorDetail(
|
|
358
|
+
Array.isArray(obj.errors) ? obj.errors : obj.error || obj.result,
|
|
359
|
+
'Unknown CLI error'
|
|
360
|
+
);
|
|
361
|
+
return { ...detail, provider: 'claude' };
|
|
362
|
+
}
|
|
363
|
+
if (obj.subtype !== 'error') return null;
|
|
364
|
+
return {
|
|
365
|
+
...cliErrorDetail(obj.error || obj.result, 'CLI returned error'),
|
|
366
|
+
provider: 'claude',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function codexFailureFromObject(obj) {
|
|
371
|
+
if (obj.type !== 'turn.failed') return null;
|
|
372
|
+
return {
|
|
373
|
+
...cliErrorDetail(obj.error?.message || obj.error?.code || obj.error, 'Turn failed'),
|
|
374
|
+
provider: 'codex',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function geminiFailureFromObject(obj) {
|
|
379
|
+
const geminiFailure =
|
|
380
|
+
(obj.type === 'result' && obj.status === 'error') ||
|
|
381
|
+
(obj.type === 'error' && obj.severity === 'error');
|
|
382
|
+
if (!geminiFailure) return null;
|
|
383
|
+
return {
|
|
384
|
+
...cliErrorDetail(obj.error?.message || obj.message, 'Gemini CLI error'),
|
|
385
|
+
provider: 'gemini',
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function opencodeFailureFromObject(obj) {
|
|
390
|
+
if (obj.type !== 'session.error' && obj.type !== 'error') return null;
|
|
391
|
+
return {
|
|
392
|
+
...cliErrorDetail(
|
|
393
|
+
obj.error?.data?.message || obj.error?.message || obj.error?.name,
|
|
394
|
+
'Session error'
|
|
395
|
+
),
|
|
396
|
+
provider: 'opencode',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function failureFromProviderObject(obj, providerName) {
|
|
401
|
+
if (providerName === 'claude') return claudeFailureFromObject(obj);
|
|
402
|
+
if (providerName === 'codex') return codexFailureFromObject(obj);
|
|
403
|
+
if (providerName === 'gemini') return geminiFailureFromObject(obj);
|
|
404
|
+
if (providerName === 'opencode') return opencodeFailureFromObject(obj);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function extractCliFailure(output, providerName = 'claude') {
|
|
312
409
|
if (!output || typeof output !== 'string') return null;
|
|
313
410
|
|
|
314
411
|
const lines = output.split('\n');
|
|
412
|
+
// Codex terminal truth is a turn.failed record at the newest end of JSONL output.
|
|
413
|
+
// Search it newest-first so a preserved terminal tail wins over older provider chatter.
|
|
414
|
+
if (providerName === 'codex') lines.reverse();
|
|
315
415
|
|
|
316
416
|
for (const line of lines) {
|
|
317
417
|
const content = stripTimestamp(line);
|
|
318
418
|
if (!content.startsWith('{')) continue;
|
|
319
|
-
|
|
320
|
-
let obj;
|
|
321
419
|
try {
|
|
322
|
-
|
|
420
|
+
const failure = failureFromProviderObject(JSON.parse(content), providerName);
|
|
421
|
+
if (failure) return failure;
|
|
323
422
|
} catch {
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (providerName === 'claude' && obj.type === 'result' && obj.is_error === true) {
|
|
328
|
-
const errorMsg = Array.isArray(obj.errors)
|
|
329
|
-
? obj.errors.join('; ')
|
|
330
|
-
: obj.error || obj.result || 'Unknown CLI error';
|
|
331
|
-
return { error: errorMsg, provider: 'claude' };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (providerName === 'claude' && obj.type === 'result' && obj.subtype === 'error') {
|
|
335
|
-
const errorMsg = obj.error || obj.result || 'CLI returned error';
|
|
336
|
-
return { error: errorMsg, provider: 'claude' };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (providerName === 'codex' && obj.type === 'turn.failed') {
|
|
340
|
-
const errorMsg = obj.error?.message || obj.error || 'Turn failed';
|
|
341
|
-
return { error: errorMsg, provider: 'codex' };
|
|
342
|
-
}
|
|
343
|
-
|
|
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
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (providerName === 'opencode' && (obj.type === 'session.error' || obj.type === 'error')) {
|
|
356
|
-
const errorMsg =
|
|
357
|
-
obj.error?.data?.message || obj.error?.message || obj.error?.name || 'Session error';
|
|
358
|
-
return { error: errorMsg, provider: 'opencode' };
|
|
423
|
+
// Ignore non-JSON output lines.
|
|
359
424
|
}
|
|
360
425
|
}
|
|
361
426
|
|
|
362
427
|
return null;
|
|
363
428
|
}
|
|
364
429
|
|
|
430
|
+
function extractCliError(output, providerName = 'claude') {
|
|
431
|
+
const failure = extractCliFailure(output, providerName);
|
|
432
|
+
return failure ? { error: failure.error, provider: failure.provider } : null;
|
|
433
|
+
}
|
|
434
|
+
|
|
365
435
|
/**
|
|
366
436
|
* Detects fatal standalone output lines that indicate no task output was produced.
|
|
367
437
|
* Only matches when the line itself is the fatal message (not when it appears inside JSON).
|
|
@@ -419,8 +489,10 @@ function extractJsonFromOutput(output, providerName = 'claude') {
|
|
|
419
489
|
}
|
|
420
490
|
|
|
421
491
|
module.exports = {
|
|
492
|
+
MAX_CLI_ERROR_BYTES,
|
|
422
493
|
extractJsonFromOutput,
|
|
423
494
|
extractModelTextFromOutput,
|
|
495
|
+
extractCliFailure,
|
|
424
496
|
extractCliError,
|
|
425
497
|
extractClaudeVertexModelError,
|
|
426
498
|
extractFromResultWrapper,
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
|
|
3
|
+
const { getProvider } = require('../providers');
|
|
4
|
+
const { extractCliFailure } = require('./output-extraction');
|
|
5
|
+
|
|
6
|
+
function categoryForProviderFailure(error, classification) {
|
|
7
|
+
const isPermanent = classification.retryable === false;
|
|
8
|
+
const authenticationPattern =
|
|
9
|
+
/(?:invalid[_ -]?api[_ -]?key|api[_ -]?key.*invalid|unauthori[sz]ed|forbidden|authentication|permission denied)/i;
|
|
10
|
+
if (isPermanent && authenticationPattern.test(error)) return 'authentication';
|
|
11
|
+
|
|
12
|
+
const quotaPattern = /(?:insufficient[_ -]?quota|quota exceeded|resource_exhausted)/i;
|
|
13
|
+
if (isPermanent && quotaPattern.test(error)) return 'quota';
|
|
14
|
+
if (isPermanent) return 'permanent';
|
|
15
|
+
return classification.kind === 'unknown-retryable' ? 'unknown' : 'transient';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function classifyProviderFailure(providerName, error) {
|
|
19
|
+
let rawClassification = { retryable: true, kind: 'unknown-retryable' };
|
|
20
|
+
try {
|
|
21
|
+
rawClassification = getProvider(providerName).adapter.classifyError(new Error(error));
|
|
22
|
+
} catch {
|
|
23
|
+
// Extraction remains available if a provider adapter cannot be loaded.
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
retryable: rawClassification?.retryable !== false,
|
|
27
|
+
kind:
|
|
28
|
+
typeof rawClassification?.kind === 'string' ? rawClassification.kind : 'unknown-retryable',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractProviderFailure(output, providerName) {
|
|
33
|
+
const cliError = extractCliFailure(output, providerName);
|
|
34
|
+
if (!cliError) return null;
|
|
35
|
+
|
|
36
|
+
const classification = classifyProviderFailure(providerName, cliError.error);
|
|
37
|
+
const category = categoryForProviderFailure(cliError.error, classification);
|
|
38
|
+
return {
|
|
39
|
+
error: `Provider ${cliError.provider} failed (${category}; ${classification.kind})`,
|
|
40
|
+
provider: cliError.provider,
|
|
41
|
+
event: cliError.provider === 'codex' ? 'turn.failed' : 'terminal_error',
|
|
42
|
+
category,
|
|
43
|
+
classification,
|
|
44
|
+
diagnostic: cliError.diagnostic,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function redactTerminalFailureForControlPlane(state, providerName, content) {
|
|
49
|
+
const failure = extractProviderFailure(content, providerName);
|
|
50
|
+
if (!failure) return content;
|
|
51
|
+
|
|
52
|
+
state.providerFailure = failure;
|
|
53
|
+
let eventType = 'provider.failure';
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(content);
|
|
56
|
+
if (typeof parsed?.type === 'string') eventType = parsed.type;
|
|
57
|
+
} catch {
|
|
58
|
+
// extractProviderFailure already proved a supported terminal envelope.
|
|
59
|
+
}
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
type: eventType,
|
|
62
|
+
...(providerName === 'claude' ? { is_error: true } : {}),
|
|
63
|
+
...(providerName === 'gemini' ? { status: 'error', severity: 'error' } : {}),
|
|
64
|
+
error: { message: failure.error },
|
|
65
|
+
zeroshot_failure: {
|
|
66
|
+
provider: failure.provider,
|
|
67
|
+
event: failure.event,
|
|
68
|
+
category: failure.category,
|
|
69
|
+
kind: failure.classification.kind,
|
|
70
|
+
retryable: failure.classification.retryable,
|
|
71
|
+
diagnostic: failure.diagnostic,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function decorateError(error, failure) {
|
|
77
|
+
if (!failure) return error;
|
|
78
|
+
error.provider = failure.provider || null;
|
|
79
|
+
error.providerEvent = failure.event || null;
|
|
80
|
+
error.providerCategory = failure.category || null;
|
|
81
|
+
error.classification = failure.classification || null;
|
|
82
|
+
error.providerDiagnostic = failure.diagnostic || null;
|
|
83
|
+
if (failure.classification?.retryable === false) error.permanent = true;
|
|
84
|
+
return error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function receiptFields(error) {
|
|
88
|
+
if (!error?.provider) return {};
|
|
89
|
+
return {
|
|
90
|
+
provider: error.provider,
|
|
91
|
+
event: error.providerEvent,
|
|
92
|
+
category: error.providerCategory,
|
|
93
|
+
kind: error.classification?.kind,
|
|
94
|
+
retryable: error.classification?.retryable,
|
|
95
|
+
diagnostic: error.providerDiagnostic,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function workerFailure(error) {
|
|
100
|
+
const authenticationFailure =
|
|
101
|
+
error?.provider &&
|
|
102
|
+
error?.classification?.retryable === false &&
|
|
103
|
+
error?.providerCategory === 'authentication';
|
|
104
|
+
return authenticationFailure
|
|
105
|
+
? { code: 'refusal', reason: 'authentication_required' }
|
|
106
|
+
: { code: 'crash', reason: 'declared_failure' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function publishCriticalFailure({
|
|
110
|
+
agent,
|
|
111
|
+
error,
|
|
112
|
+
attempts,
|
|
113
|
+
worker,
|
|
114
|
+
unsupportedCapability,
|
|
115
|
+
structuredOutputInvalid,
|
|
116
|
+
}) {
|
|
117
|
+
const specific =
|
|
118
|
+
error?.hookFailure ||
|
|
119
|
+
structuredOutputInvalid ||
|
|
120
|
+
unsupportedCapability ||
|
|
121
|
+
error?.vertexModelError ||
|
|
122
|
+
error?.terminationExhausted;
|
|
123
|
+
const critical =
|
|
124
|
+
agent.role === 'implementation' ||
|
|
125
|
+
agent.role === 'coordinator' ||
|
|
126
|
+
agent.id === 'consensus-coordinator';
|
|
127
|
+
if (!critical || specific) return worker;
|
|
128
|
+
|
|
129
|
+
agent._publish({
|
|
130
|
+
topic: 'CLUSTER_FAILED',
|
|
131
|
+
receiver: 'broadcast',
|
|
132
|
+
content: {
|
|
133
|
+
text: `Critical agent ${agent.id} exhausted its retry budget`,
|
|
134
|
+
data: {
|
|
135
|
+
reason: error?.provider ? 'provider_execution_failed' : 'critical_agent_exhausted',
|
|
136
|
+
agentId: agent.id,
|
|
137
|
+
role: agent.role,
|
|
138
|
+
attempts,
|
|
139
|
+
code: worker.code,
|
|
140
|
+
workerReason: worker.reason,
|
|
141
|
+
...receiptFields(error),
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
return worker;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildFinalFailureInfo({
|
|
149
|
+
agent,
|
|
150
|
+
error,
|
|
151
|
+
attempts,
|
|
152
|
+
worker,
|
|
153
|
+
unsupportedCapability,
|
|
154
|
+
structuredOutputInvalid,
|
|
155
|
+
}) {
|
|
156
|
+
return {
|
|
157
|
+
...(error?.terminationExhausted ? agent.cluster.failureInfo : {}),
|
|
158
|
+
agentId: agent.id,
|
|
159
|
+
taskId: error?.taskId || agent.currentTaskId,
|
|
160
|
+
iteration: agent.iteration,
|
|
161
|
+
error: error.message,
|
|
162
|
+
attempts,
|
|
163
|
+
...receiptFields(error),
|
|
164
|
+
...(error?.provider ? { code: worker.code, workerReason: worker.reason } : {}),
|
|
165
|
+
...(unsupportedCapability
|
|
166
|
+
? {
|
|
167
|
+
code: error.code,
|
|
168
|
+
permanent: true,
|
|
169
|
+
provider: error.provider,
|
|
170
|
+
capability: error.capability,
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
...(structuredOutputInvalid ? { code: error.code, details: error.details ?? null } : {}),
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
buildFinalFailureInfo,
|
|
180
|
+
decorateError,
|
|
181
|
+
extractProviderFailure,
|
|
182
|
+
publishCriticalFailure,
|
|
183
|
+
receiptFields,
|
|
184
|
+
redactTerminalFailureForControlPlane,
|
|
185
|
+
workerFailure,
|
|
186
|
+
};
|