@covibes/zeroshot 5.2.1 → 5.3.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 +174 -189
- package/README.md +199 -248
- package/cli/commands/providers.js +150 -0
- package/cli/index.js +214 -58
- package/cli/lib/first-run.js +40 -3
- package/cluster-templates/base-templates/debug-workflow.json +24 -78
- package/cluster-templates/base-templates/full-workflow.json +44 -145
- package/cluster-templates/base-templates/single-worker.json +23 -15
- package/cluster-templates/base-templates/worker-validator.json +47 -34
- package/cluster-templates/conductor-bootstrap.json +7 -5
- package/lib/docker-config.js +6 -1
- package/lib/provider-detection.js +59 -0
- package/lib/provider-names.js +56 -0
- package/lib/settings.js +191 -6
- package/lib/stream-json-parser.js +4 -238
- package/package.json +21 -5
- package/scripts/validate-templates.js +100 -0
- package/src/agent/agent-config.js +37 -13
- package/src/agent/agent-context-builder.js +64 -2
- package/src/agent/agent-hook-executor.js +82 -9
- package/src/agent/agent-lifecycle.js +53 -14
- package/src/agent/agent-task-executor.js +196 -194
- package/src/agent/output-extraction.js +200 -0
- package/src/agent/output-reformatter.js +175 -0
- package/src/agent/schema-utils.js +111 -0
- package/src/agent-wrapper.js +102 -30
- package/src/agents/git-pusher-agent.json +1 -1
- package/src/claude-task-runner.js +80 -30
- package/src/config-router.js +13 -13
- package/src/config-validator.js +231 -10
- package/src/github.js +36 -0
- package/src/isolation-manager.js +243 -154
- package/src/ledger.js +28 -6
- package/src/orchestrator.js +391 -96
- package/src/preflight.js +85 -82
- package/src/providers/anthropic/cli-builder.js +45 -0
- package/src/providers/anthropic/index.js +134 -0
- package/src/providers/anthropic/models.js +23 -0
- package/src/providers/anthropic/output-parser.js +159 -0
- package/src/providers/base-provider.js +181 -0
- package/src/providers/capabilities.js +51 -0
- package/src/providers/google/cli-builder.js +55 -0
- package/src/providers/google/index.js +116 -0
- package/src/providers/google/models.js +24 -0
- package/src/providers/google/output-parser.js +92 -0
- package/src/providers/index.js +75 -0
- package/src/providers/openai/cli-builder.js +122 -0
- package/src/providers/openai/index.js +135 -0
- package/src/providers/openai/models.js +21 -0
- package/src/providers/openai/output-parser.js +129 -0
- package/src/sub-cluster-wrapper.js +18 -3
- package/src/task-runner.js +8 -6
- package/src/tui/layout.js +20 -3
- package/task-lib/attachable-watcher.js +80 -78
- package/task-lib/claude-recovery.js +119 -0
- package/task-lib/commands/list.js +1 -1
- package/task-lib/commands/resume.js +3 -2
- package/task-lib/commands/run.js +12 -3
- package/task-lib/runner.js +59 -38
- package/task-lib/scheduler.js +2 -2
- package/task-lib/store.js +43 -30
- package/task-lib/watcher.js +81 -62
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* - maxModel ceiling enforcement at config time
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const { loadSettings, validateModelAgainstMax } = require('../../lib/settings');
|
|
12
|
+
const { loadSettings, validateModelAgainstMax, VALID_MODELS } = require('../../lib/settings');
|
|
13
|
+
|
|
14
|
+
const VALID_LEVELS = ['level1', 'level2', 'level3'];
|
|
13
15
|
|
|
14
16
|
// Default max iterations (high limit - let the user decide when to give up)
|
|
15
17
|
const DEFAULT_MAX_ITERATIONS = 100;
|
|
@@ -58,39 +60,61 @@ function validateAgentConfig(config, options = {}) {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
// Model configuration: support both static model and dynamic rules
|
|
61
|
-
// If no model specified, model is null - _selectModel() will use
|
|
63
|
+
// If no model specified, model is null - _selectModel() will use provider defaults
|
|
62
64
|
let modelConfig;
|
|
63
65
|
if (config.modelRules) {
|
|
64
66
|
modelConfig = { type: 'rules', rules: config.modelRules };
|
|
65
67
|
} else {
|
|
66
|
-
modelConfig = {
|
|
68
|
+
modelConfig = {
|
|
69
|
+
type: 'static',
|
|
70
|
+
model: config.model || null,
|
|
71
|
+
modelLevel: config.modelLevel || null,
|
|
72
|
+
};
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
// COST CEILING ENFORCEMENT: Validate model(s) against maxModel at config time
|
|
75
|
+
// COST CEILING/FLOOR ENFORCEMENT: Validate model(s) against maxModel and minModel at config time
|
|
70
76
|
// Catches violations EARLY (config load) instead of at runtime (iteration N)
|
|
71
77
|
const settings = loadSettings();
|
|
72
78
|
const maxModel = settings.maxModel || 'sonnet';
|
|
79
|
+
const minModel = settings.minModel || null;
|
|
80
|
+
|
|
81
|
+
if (modelConfig.type === 'static') {
|
|
82
|
+
if (modelConfig.model && VALID_MODELS.includes(modelConfig.model)) {
|
|
83
|
+
// Static model: validate once (legacy Claude models only)
|
|
84
|
+
try {
|
|
85
|
+
validateModelAgainstMax(modelConfig.model, maxModel, minModel);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Agent "${config.id}": ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
throw new Error(`Agent "${config.id}": ${error.message}`);
|
|
91
|
+
if (modelConfig.modelLevel && !VALID_LEVELS.includes(modelConfig.modelLevel)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Agent "${config.id}": invalid modelLevel "${modelConfig.modelLevel}". ` +
|
|
94
|
+
`Valid: ${VALID_LEVELS.join(', ')}`
|
|
95
|
+
);
|
|
80
96
|
}
|
|
81
97
|
} else if (modelConfig.type === 'rules') {
|
|
82
98
|
// Dynamic rules: validate ALL rules upfront (don't wait until iteration N)
|
|
83
99
|
for (const rule of modelConfig.rules) {
|
|
84
|
-
if (rule.model) {
|
|
100
|
+
if (rule.model && VALID_MODELS.includes(rule.model)) {
|
|
85
101
|
try {
|
|
86
|
-
validateModelAgainstMax(rule.model, maxModel);
|
|
102
|
+
validateModelAgainstMax(rule.model, maxModel, minModel);
|
|
87
103
|
} catch {
|
|
88
104
|
throw new Error(
|
|
89
105
|
`Agent "${config.id}": modelRule "${rule.iterations}" requests "${rule.model}" ` +
|
|
90
|
-
`but maxModel is "${maxModel}"
|
|
106
|
+
`but maxModel is "${maxModel}"${minModel ? ` and minModel is "${minModel}"` : ''}. ` +
|
|
107
|
+
`Either adjust the rule's model or change maxModel/minModel settings.`
|
|
91
108
|
);
|
|
92
109
|
}
|
|
93
110
|
}
|
|
111
|
+
|
|
112
|
+
if (rule.modelLevel && !VALID_LEVELS.includes(rule.modelLevel)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Agent "${config.id}": modelRule "${rule.iterations}" has invalid modelLevel ` +
|
|
115
|
+
`"${rule.modelLevel}". Valid: ${VALID_LEVELS.join(', ')}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
94
118
|
}
|
|
95
119
|
}
|
|
96
120
|
|
|
@@ -13,6 +13,44 @@
|
|
|
13
13
|
// Prevents "Prompt is too long" errors that kill tasks
|
|
14
14
|
const MAX_CONTEXT_CHARS = 500000;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Generate an example object from a JSON schema
|
|
18
|
+
* Used to show models a concrete example of expected output
|
|
19
|
+
*
|
|
20
|
+
* @param {object} schema - JSON schema
|
|
21
|
+
* @returns {object|null} Example object or null if generation fails
|
|
22
|
+
*/
|
|
23
|
+
function generateExampleFromSchema(schema) {
|
|
24
|
+
if (!schema || schema.type !== 'object' || !schema.properties) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const example = {};
|
|
29
|
+
|
|
30
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
31
|
+
if (propSchema.enum && propSchema.enum.length > 0) {
|
|
32
|
+
// Use first enum value as example
|
|
33
|
+
example[key] = propSchema.enum[0];
|
|
34
|
+
} else if (propSchema.type === 'string') {
|
|
35
|
+
example[key] = propSchema.description || `${key} value`;
|
|
36
|
+
} else if (propSchema.type === 'boolean') {
|
|
37
|
+
example[key] = true;
|
|
38
|
+
} else if (propSchema.type === 'number' || propSchema.type === 'integer') {
|
|
39
|
+
example[key] = 0;
|
|
40
|
+
} else if (propSchema.type === 'array') {
|
|
41
|
+
if (propSchema.items?.type === 'string') {
|
|
42
|
+
example[key] = [];
|
|
43
|
+
} else {
|
|
44
|
+
example[key] = [];
|
|
45
|
+
}
|
|
46
|
+
} else if (propSchema.type === 'object') {
|
|
47
|
+
example[key] = generateExampleFromSchema(propSchema) || {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return example;
|
|
52
|
+
}
|
|
53
|
+
|
|
16
54
|
/**
|
|
17
55
|
* Build execution context for an agent
|
|
18
56
|
* @param {object} params - Context building parameters
|
|
@@ -91,7 +129,8 @@ function buildContext({
|
|
|
91
129
|
// Add prompt from config (system prompt, instructions, output format)
|
|
92
130
|
// If selectedPrompt is provided (iteration-based), use it directly
|
|
93
131
|
// Otherwise fall back to legacy config.prompt handling
|
|
94
|
-
const promptText =
|
|
132
|
+
const promptText =
|
|
133
|
+
selectedPrompt || (typeof config.prompt === 'string' ? config.prompt : config.prompt?.system);
|
|
95
134
|
|
|
96
135
|
if (promptText) {
|
|
97
136
|
context += `## Instructions\n\n${promptText}\n\n`;
|
|
@@ -103,7 +142,7 @@ function buildContext({
|
|
|
103
142
|
);
|
|
104
143
|
}
|
|
105
144
|
|
|
106
|
-
// Output format schema (if configured)
|
|
145
|
+
// Output format schema (if configured via legacy format)
|
|
107
146
|
if (config.prompt?.outputFormat) {
|
|
108
147
|
context += `## Output Schema (REQUIRED)\n\n`;
|
|
109
148
|
context += `\`\`\`json\n${JSON.stringify(config.prompt.outputFormat.example, null, 2)}\n\`\`\`\n\n`;
|
|
@@ -116,6 +155,29 @@ function buildContext({
|
|
|
116
155
|
context += '\n';
|
|
117
156
|
}
|
|
118
157
|
|
|
158
|
+
// AUTO-INJECT JSON OUTPUT INSTRUCTIONS when jsonSchema is defined
|
|
159
|
+
// This ensures ALL agents with structured output schemas get explicit "output ONLY JSON" instructions
|
|
160
|
+
// Critical for less capable models (Codex, Gemini) that output prose without explicit instructions
|
|
161
|
+
if (config.jsonSchema && config.outputFormat === 'json') {
|
|
162
|
+
context += `## 🔴 OUTPUT FORMAT - JSON ONLY\n\n`;
|
|
163
|
+
context += `Your response must be ONLY valid JSON. No other text before or after.\n`;
|
|
164
|
+
context += `Start with { and end with }. Nothing else.\n\n`;
|
|
165
|
+
context += `Required schema:\n`;
|
|
166
|
+
context += `\`\`\`json\n${JSON.stringify(config.jsonSchema, null, 2)}\n\`\`\`\n\n`;
|
|
167
|
+
|
|
168
|
+
// Generate example from schema
|
|
169
|
+
const example = generateExampleFromSchema(config.jsonSchema);
|
|
170
|
+
if (example) {
|
|
171
|
+
context += `Example output:\n`;
|
|
172
|
+
context += `\`\`\`json\n${JSON.stringify(example, null, 2)}\n\`\`\`\n\n`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
context += `CRITICAL RULES:\n`;
|
|
176
|
+
context += `- Output ONLY the JSON object - no explanation, no thinking, no preamble\n`;
|
|
177
|
+
context += `- Use EXACTLY the enum values specified (case-sensitive)\n`;
|
|
178
|
+
context += `- Include ALL required fields\n\n`;
|
|
179
|
+
}
|
|
180
|
+
|
|
119
181
|
// Add sources
|
|
120
182
|
for (const source of strategy.sources) {
|
|
121
183
|
// Resolve special 'since' values
|
|
@@ -22,7 +22,7 @@ const vm = require('vm');
|
|
|
22
22
|
* @param {Object} params.orchestrator - Orchestrator instance
|
|
23
23
|
* @returns {Promise<void>}
|
|
24
24
|
*/
|
|
25
|
-
function executeHook(params) {
|
|
25
|
+
async function executeHook(params) {
|
|
26
26
|
const { hook, agent, message, result, cluster } = params;
|
|
27
27
|
|
|
28
28
|
if (!hook) {
|
|
@@ -43,14 +43,14 @@ function executeHook(params) {
|
|
|
43
43
|
|
|
44
44
|
if (hook.transform) {
|
|
45
45
|
// NEW: Execute transform script to generate message
|
|
46
|
-
messageToPublish = executeTransform({
|
|
46
|
+
messageToPublish = await executeTransform({
|
|
47
47
|
transform: hook.transform,
|
|
48
48
|
context,
|
|
49
49
|
agent,
|
|
50
50
|
});
|
|
51
51
|
} else {
|
|
52
52
|
// Existing: Use template substitution
|
|
53
|
-
messageToPublish = substituteTemplate({
|
|
53
|
+
messageToPublish = await substituteTemplate({
|
|
54
54
|
config: hook.config,
|
|
55
55
|
context,
|
|
56
56
|
agent,
|
|
@@ -77,9 +77,9 @@ function executeHook(params) {
|
|
|
77
77
|
* @param {Object} params.transform - Transform configuration
|
|
78
78
|
* @param {Object} params.context - Execution context
|
|
79
79
|
* @param {Object} params.agent - Agent instance
|
|
80
|
-
* @returns {Object} Message to publish
|
|
80
|
+
* @returns {Promise<Object>} Message to publish
|
|
81
81
|
*/
|
|
82
|
-
function executeTransform(params) {
|
|
82
|
+
async function executeTransform(params) {
|
|
83
83
|
const { transform, context, agent } = params;
|
|
84
84
|
const { engine, script } = transform;
|
|
85
85
|
|
|
@@ -93,7 +93,46 @@ function executeTransform(params) {
|
|
|
93
93
|
let resultData = null;
|
|
94
94
|
|
|
95
95
|
if (context.result?.output) {
|
|
96
|
-
|
|
96
|
+
try {
|
|
97
|
+
resultData = await agent._parseResultOutput(context.result.output);
|
|
98
|
+
} catch (parseError) {
|
|
99
|
+
// FAIL FAST: Result parsing failed - don't continue with null data
|
|
100
|
+
const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
|
|
101
|
+
console.error(`\n${'='.repeat(80)}`);
|
|
102
|
+
console.error(`🔴 TRANSFORM SCRIPT BLOCKED - RESULT PARSING FAILED`);
|
|
103
|
+
console.error(`${'='.repeat(80)}`);
|
|
104
|
+
console.error(`Agent: ${agent.id}, Role: ${agent.role}`);
|
|
105
|
+
console.error(`TaskID: ${taskId}`);
|
|
106
|
+
console.error(`Parse error: ${parseError.message}`);
|
|
107
|
+
console.error(`Output (last 500 chars): ${(context.result.output || '').slice(-500)}`);
|
|
108
|
+
console.error(`${'='.repeat(80)}\n`);
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Transform script cannot run: result parsing failed. ` +
|
|
111
|
+
`Agent: ${agent.id}, Error: ${parseError.message}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// DEFENSIVE: Validate result has expected fields if script accesses them
|
|
116
|
+
// Extract field names from script (e.g., result.complexity, result.taskType)
|
|
117
|
+
const accessedFields = [...script.matchAll(/result\.([a-zA-Z_]+)/g)].map((m) => m[1]);
|
|
118
|
+
const missingFields = accessedFields.filter((f) => resultData[f] === undefined);
|
|
119
|
+
if (missingFields.length > 0) {
|
|
120
|
+
const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
|
|
121
|
+
console.error(`\n${'='.repeat(80)}`);
|
|
122
|
+
console.error(`🔴 TRANSFORM SCRIPT BLOCKED - MISSING REQUIRED FIELDS`);
|
|
123
|
+
console.error(`${'='.repeat(80)}`);
|
|
124
|
+
console.error(`Agent: ${agent.id}, Role: ${agent.role}, TaskID: ${taskId}`);
|
|
125
|
+
console.error(`Script accesses: ${accessedFields.join(', ')}`);
|
|
126
|
+
console.error(`Missing from result: ${missingFields.join(', ')}`);
|
|
127
|
+
console.error(`Result keys: ${Object.keys(resultData).join(', ')}`);
|
|
128
|
+
console.error(`Result data: ${JSON.stringify(resultData, null, 2)}`);
|
|
129
|
+
console.error(`${'='.repeat(80)}\n`);
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Transform script accesses undefined fields: ${missingFields.join(', ')}. ` +
|
|
132
|
+
`Agent ${agent.id} (task ${taskId}) output missing required fields. ` +
|
|
133
|
+
`Check agent's jsonSchema and output format.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
97
136
|
} else if (scriptUsesResult) {
|
|
98
137
|
const taskId = context.result?.taskId || agent.currentTaskId || 'UNKNOWN';
|
|
99
138
|
const outputLength = (context.result?.output || '').length;
|
|
@@ -151,6 +190,40 @@ function executeTransform(params) {
|
|
|
151
190
|
throw new Error(`Transform script result must have a 'content' property`);
|
|
152
191
|
}
|
|
153
192
|
|
|
193
|
+
// CRITICAL: Extra validation for CLUSTER_OPERATIONS - this is the make-or-break message
|
|
194
|
+
// If this message is malformed, the cluster will hang forever
|
|
195
|
+
if (result.topic === 'CLUSTER_OPERATIONS') {
|
|
196
|
+
const operations = result.content?.data?.operations;
|
|
197
|
+
if (!operations) {
|
|
198
|
+
console.error(`\n${'='.repeat(80)}`);
|
|
199
|
+
console.error(`🔴 CLUSTER_OPERATIONS MALFORMED - MISSING OPERATIONS ARRAY`);
|
|
200
|
+
console.error(`${'='.repeat(80)}`);
|
|
201
|
+
console.error(`Agent: ${agent.id}`);
|
|
202
|
+
console.error(`Result: ${JSON.stringify(result, null, 2)}`);
|
|
203
|
+
console.error(`${'='.repeat(80)}\n`);
|
|
204
|
+
throw new Error(
|
|
205
|
+
`CLUSTER_OPERATIONS message missing operations array. ` +
|
|
206
|
+
`Agent ${agent.id} transform script returned invalid structure.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!Array.isArray(operations)) {
|
|
210
|
+
throw new Error(`CLUSTER_OPERATIONS.operations must be an array, got: ${typeof operations}`);
|
|
211
|
+
}
|
|
212
|
+
if (operations.length === 0) {
|
|
213
|
+
throw new Error(`CLUSTER_OPERATIONS.operations is empty - no operations to execute`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Validate each operation has required 'action' field
|
|
217
|
+
for (let i = 0; i < operations.length; i++) {
|
|
218
|
+
const op = operations[i];
|
|
219
|
+
if (!op || !op.action) {
|
|
220
|
+
throw new Error(`CLUSTER_OPERATIONS.operations[${i}] missing required 'action' field`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
agent._log(`✅ CLUSTER_OPERATIONS validated: ${operations.length} operations`);
|
|
225
|
+
}
|
|
226
|
+
|
|
154
227
|
return result;
|
|
155
228
|
}
|
|
156
229
|
|
|
@@ -163,9 +236,9 @@ function executeTransform(params) {
|
|
|
163
236
|
* @param {Object} params.context - Execution context
|
|
164
237
|
* @param {Object} params.agent - Agent instance
|
|
165
238
|
* @param {Object} params.cluster - Cluster object
|
|
166
|
-
* @returns {Object} Substituted configuration
|
|
239
|
+
* @returns {Promise<Object>} Substituted configuration
|
|
167
240
|
*/
|
|
168
|
-
function substituteTemplate(params) {
|
|
241
|
+
async function substituteTemplate(params) {
|
|
169
242
|
const { config, context, agent, cluster } = params;
|
|
170
243
|
|
|
171
244
|
if (!config) {
|
|
@@ -243,7 +316,7 @@ function substituteTemplate(params) {
|
|
|
243
316
|
);
|
|
244
317
|
}
|
|
245
318
|
// Parse result output - WILL THROW if no JSON block
|
|
246
|
-
resultData = agent._parseResultOutput(context.result.output);
|
|
319
|
+
resultData = await agent._parseResultOutput(context.result.output);
|
|
247
320
|
}
|
|
248
321
|
|
|
249
322
|
// Helper to escape a value for JSON string substitution
|
|
@@ -281,7 +281,7 @@ async function executeTask(agent, triggeringMessage) {
|
|
|
281
281
|
console.log(`${'='.repeat(80)}\n`);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
// Spawn
|
|
284
|
+
// Spawn provider task
|
|
285
285
|
agent.state = 'executing_task';
|
|
286
286
|
|
|
287
287
|
// LOCK CONTENTION FIX: Add random jitter for validators to prevent thundering herd
|
|
@@ -292,14 +292,19 @@ async function executeTask(agent, triggeringMessage) {
|
|
|
292
292
|
if (agent.role === 'validator' && !agent.testMode) {
|
|
293
293
|
const jitterMs = Math.floor(Math.random() * 15000); // 0-15 seconds
|
|
294
294
|
if (!agent.quiet) {
|
|
295
|
-
agent._log(
|
|
295
|
+
agent._log(
|
|
296
|
+
`[Agent ${agent.id}] Adding ${Math.round(jitterMs / 1000)}s jitter to prevent lock contention`
|
|
297
|
+
);
|
|
296
298
|
}
|
|
297
299
|
await new Promise((resolve) => setTimeout(resolve, jitterMs));
|
|
298
300
|
}
|
|
299
301
|
|
|
302
|
+
const modelSpec = agent._resolveModelSpec ? agent._resolveModelSpec() : null;
|
|
300
303
|
agent._publishLifecycle('TASK_STARTED', {
|
|
301
304
|
iteration: agent.iteration,
|
|
302
305
|
model: agent._selectModel(),
|
|
306
|
+
provider: agent._resolveProvider ? agent._resolveProvider() : 'claude',
|
|
307
|
+
modelSpec,
|
|
303
308
|
triggeredBy: triggeringMessage.topic,
|
|
304
309
|
triggerFrom: triggeringMessage.sender,
|
|
305
310
|
});
|
|
@@ -352,16 +357,48 @@ async function executeTask(agent, triggeringMessage) {
|
|
|
352
357
|
});
|
|
353
358
|
}
|
|
354
359
|
|
|
355
|
-
// Execute onComplete hook
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
360
|
+
// Execute onComplete hook WITH RETRY
|
|
361
|
+
// Hook failure shouldn't retry the entire task - just the hook
|
|
362
|
+
const hookMaxRetries = 3;
|
|
363
|
+
const hookBaseDelay = 1000;
|
|
364
|
+
let hookSuccess = false;
|
|
365
|
+
|
|
366
|
+
for (let hookAttempt = 1; hookAttempt <= hookMaxRetries && !hookSuccess; hookAttempt++) {
|
|
367
|
+
try {
|
|
368
|
+
await executeHook({
|
|
369
|
+
hook: agent.config.hooks?.onComplete,
|
|
370
|
+
agent: agent,
|
|
371
|
+
message: triggeringMessage,
|
|
372
|
+
result: result,
|
|
373
|
+
messageBus: agent.messageBus,
|
|
374
|
+
cluster: agent.cluster,
|
|
375
|
+
orchestrator: agent.orchestrator,
|
|
376
|
+
});
|
|
377
|
+
hookSuccess = true;
|
|
378
|
+
} catch (hookError) {
|
|
379
|
+
console.error(`\n${'='.repeat(80)}`);
|
|
380
|
+
console.error(
|
|
381
|
+
`🔴 HOOK EXECUTION FAILED - AGENT: ${agent.id} (Attempt ${hookAttempt}/${hookMaxRetries})`
|
|
382
|
+
);
|
|
383
|
+
console.error(`${'='.repeat(80)}`);
|
|
384
|
+
console.error(`Error: ${hookError.message}`);
|
|
385
|
+
|
|
386
|
+
if (hookAttempt < hookMaxRetries) {
|
|
387
|
+
const delay = hookBaseDelay * Math.pow(2, hookAttempt - 1);
|
|
388
|
+
console.error(`Will retry hook in ${delay}ms...`);
|
|
389
|
+
console.error(`${'='.repeat(80)}\n`);
|
|
390
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
391
|
+
} else {
|
|
392
|
+
console.error(`${'='.repeat(80)}\n`);
|
|
393
|
+
// All hook retries exhausted - throw to trigger task-level handling
|
|
394
|
+
throw new Error(
|
|
395
|
+
`Hook execution failed after ${hookMaxRetries} attempts. ` +
|
|
396
|
+
`Task completed successfully but hook could not publish result. ` +
|
|
397
|
+
`Original error: ${hookError.message}`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
365
402
|
|
|
366
403
|
// ✅ SUCCESS - exit retry loop
|
|
367
404
|
return;
|
|
@@ -381,8 +418,10 @@ async function executeTask(agent, triggeringMessage) {
|
|
|
381
418
|
if (isLockError) {
|
|
382
419
|
// Lock contention - add significant jittered delay
|
|
383
420
|
const lockDelay = 10000 + Math.floor(Math.random() * 20000); // 10-30 seconds
|
|
384
|
-
console.error(
|
|
385
|
-
|
|
421
|
+
console.error(
|
|
422
|
+
`⚠️ Lock contention detected - waiting ${Math.round(lockDelay / 1000)}s before retry`
|
|
423
|
+
);
|
|
424
|
+
await new Promise((resolve) => setTimeout(resolve, lockDelay));
|
|
386
425
|
} else if (attempt < maxRetries) {
|
|
387
426
|
console.error(`Will retry in ${baseDelay * Math.pow(2, attempt - 1)}ms...`);
|
|
388
427
|
}
|