@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Extraction Module - Multi-Provider JSON Extraction
|
|
3
|
+
*
|
|
4
|
+
* Clean extraction pipeline for structured JSON from AI provider outputs.
|
|
5
|
+
* Each provider has different output formats - this module normalizes them.
|
|
6
|
+
*
|
|
7
|
+
* Provider formats:
|
|
8
|
+
* - Claude: {"type":"result","result":{...}} or {"type":"result","structured_output":{...}}
|
|
9
|
+
* - Codex: Raw text in item.created events, turn.completed has NO result field
|
|
10
|
+
* - Gemini: Raw text in message events, result event may have NO result field
|
|
11
|
+
*
|
|
12
|
+
* Extraction priority (most specific → least specific):
|
|
13
|
+
* 1. Result wrapper with content (type:result + result/structured_output field)
|
|
14
|
+
* 2. Accumulated text from provider parser events
|
|
15
|
+
* 3. Markdown code block extraction
|
|
16
|
+
* 4. Direct JSON parse of entire output
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { getProvider, parseChunkWithProvider } = require('../providers');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strip timestamp prefix from log lines.
|
|
23
|
+
* Format: [epochMs]content or [epochMs]{json...}
|
|
24
|
+
*
|
|
25
|
+
* @param {string} line - Raw log line
|
|
26
|
+
* @returns {string} Content without timestamp prefix
|
|
27
|
+
*/
|
|
28
|
+
function stripTimestamp(line) {
|
|
29
|
+
if (!line || typeof line !== 'string') return '';
|
|
30
|
+
const trimmed = line.trim().replace(/\r$/, '');
|
|
31
|
+
if (!trimmed) return '';
|
|
32
|
+
const match = trimmed.match(/^\[(\d{13})\](.*)$/);
|
|
33
|
+
return match ? match[2] : trimmed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Strategy 1: Extract from result wrapper
|
|
38
|
+
* Handles Claude CLI format: {"type":"result","result":{...}}
|
|
39
|
+
*
|
|
40
|
+
* @param {string} output - Raw output
|
|
41
|
+
* @returns {object|null} Extracted JSON or null
|
|
42
|
+
*/
|
|
43
|
+
function extractFromResultWrapper(output) {
|
|
44
|
+
const lines = output.split('\n');
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const content = stripTimestamp(line);
|
|
48
|
+
if (!content.startsWith('{')) continue;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const obj = JSON.parse(content);
|
|
52
|
+
|
|
53
|
+
// Must be type:result WITH actual content
|
|
54
|
+
if (obj.type !== 'result') continue;
|
|
55
|
+
|
|
56
|
+
// Check structured_output first (standard CLI format)
|
|
57
|
+
if (obj.structured_output && typeof obj.structured_output === 'object') {
|
|
58
|
+
return obj.structured_output;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check result field - can be object or string
|
|
62
|
+
if (obj.result) {
|
|
63
|
+
if (typeof obj.result === 'object') {
|
|
64
|
+
return obj.result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Result is string - might contain markdown-wrapped JSON
|
|
68
|
+
if (typeof obj.result === 'string') {
|
|
69
|
+
const extracted = extractFromMarkdown(obj.result) || extractDirectJson(obj.result);
|
|
70
|
+
if (extracted) return extracted;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Not valid JSON, continue to next line
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Strategy 2: Extract from accumulated text events
|
|
83
|
+
* Handles non-Claude providers where JSON is in text content
|
|
84
|
+
*
|
|
85
|
+
* @param {string} output - Raw output
|
|
86
|
+
* @param {string} providerName - Provider name for parser selection
|
|
87
|
+
* @returns {object|null} Extracted JSON or null
|
|
88
|
+
*/
|
|
89
|
+
function extractFromTextEvents(output, providerName) {
|
|
90
|
+
const provider = getProvider(providerName);
|
|
91
|
+
const events = parseChunkWithProvider(provider, output);
|
|
92
|
+
|
|
93
|
+
// Accumulate all text events
|
|
94
|
+
const textContent = events
|
|
95
|
+
.filter((e) => e.type === 'text')
|
|
96
|
+
.map((e) => e.text)
|
|
97
|
+
.join('');
|
|
98
|
+
|
|
99
|
+
if (!textContent.trim()) return null;
|
|
100
|
+
|
|
101
|
+
// Try parsing accumulated text as JSON
|
|
102
|
+
return extractDirectJson(textContent) || extractFromMarkdown(textContent);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Strategy 3: Extract JSON from markdown code block
|
|
107
|
+
* Handles: ```json\n{...}\n```
|
|
108
|
+
*
|
|
109
|
+
* @param {string} text - Text that may contain markdown
|
|
110
|
+
* @returns {object|null} Extracted JSON or null
|
|
111
|
+
*/
|
|
112
|
+
function extractFromMarkdown(text) {
|
|
113
|
+
if (!text) return null;
|
|
114
|
+
|
|
115
|
+
// Match ```json ... ``` with any whitespace
|
|
116
|
+
const match = text.match(/```json\s*([\s\S]*?)```/);
|
|
117
|
+
if (!match) return null;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(match[1].trim());
|
|
121
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Invalid JSON in markdown block
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Strategy 4: Direct JSON parse
|
|
133
|
+
* Handles raw JSON output (single-line or multi-line)
|
|
134
|
+
*
|
|
135
|
+
* @param {string} text - Text to parse
|
|
136
|
+
* @returns {object|null} Parsed JSON or null
|
|
137
|
+
*/
|
|
138
|
+
function extractDirectJson(text) {
|
|
139
|
+
if (!text) return null;
|
|
140
|
+
|
|
141
|
+
const trimmed = text.trim();
|
|
142
|
+
if (!trimmed) return null;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(trimmed);
|
|
146
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
147
|
+
return parsed;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Not valid JSON
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Main extraction function - tries all strategies in priority order
|
|
158
|
+
*
|
|
159
|
+
* @param {string} output - Raw output from AI provider CLI
|
|
160
|
+
* @param {string} providerName - Provider name ('claude', 'codex', 'gemini')
|
|
161
|
+
* @returns {object|null} Extracted JSON object or null if extraction failed
|
|
162
|
+
*/
|
|
163
|
+
function extractJsonFromOutput(output, providerName = 'claude') {
|
|
164
|
+
if (!output || typeof output !== 'string') return null;
|
|
165
|
+
|
|
166
|
+
const trimmedOutput = output.trim();
|
|
167
|
+
if (!trimmedOutput) return null;
|
|
168
|
+
|
|
169
|
+
// Check for fatal error indicators
|
|
170
|
+
if (trimmedOutput.includes('Task not found') || trimmedOutput.includes('Process terminated')) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Strategy 1: Result wrapper (Claude format)
|
|
175
|
+
const fromWrapper = extractFromResultWrapper(trimmedOutput);
|
|
176
|
+
if (fromWrapper) return fromWrapper;
|
|
177
|
+
|
|
178
|
+
// Strategy 2: Text events (non-Claude providers)
|
|
179
|
+
const fromText = extractFromTextEvents(trimmedOutput, providerName);
|
|
180
|
+
if (fromText) return fromText;
|
|
181
|
+
|
|
182
|
+
// Strategy 3: Markdown extraction
|
|
183
|
+
const fromMarkdown = extractFromMarkdown(trimmedOutput);
|
|
184
|
+
if (fromMarkdown) return fromMarkdown;
|
|
185
|
+
|
|
186
|
+
// Strategy 4: Direct JSON parse (raw output)
|
|
187
|
+
const fromDirect = extractDirectJson(trimmedOutput);
|
|
188
|
+
if (fromDirect) return fromDirect;
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
extractJsonFromOutput,
|
|
195
|
+
extractFromResultWrapper,
|
|
196
|
+
extractFromTextEvents,
|
|
197
|
+
extractFromMarkdown,
|
|
198
|
+
extractDirectJson,
|
|
199
|
+
stripTimestamp,
|
|
200
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Reformatter - Convert non-JSON output to valid JSON
|
|
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
|
+
* STATUS: SDK NOT IMPLEMENTED - Reformatting is not available.
|
|
8
|
+
* This module exists for future extension when SDK support is added.
|
|
9
|
+
*
|
|
10
|
+
* To enable reformatting:
|
|
11
|
+
* 1. Implement SDK support in the provider (getSDKEnvVar, callSimple)
|
|
12
|
+
* 2. The reformatOutput() function will then work automatically
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the reformatting prompt
|
|
19
|
+
*
|
|
20
|
+
* @param {string} rawOutput - The non-JSON output to reformat
|
|
21
|
+
* @param {Object} schema - Target JSON schema
|
|
22
|
+
* @param {string|null} previousError - Error from previous attempt (for feedback)
|
|
23
|
+
* @returns {string} The prompt for the reformatting model
|
|
24
|
+
*/
|
|
25
|
+
function buildReformatPrompt(rawOutput, schema, previousError = null) {
|
|
26
|
+
const schemaStr = JSON.stringify(schema, null, 2);
|
|
27
|
+
// Truncate long outputs to avoid context limits
|
|
28
|
+
const truncatedOutput = rawOutput.length > 4000 ? rawOutput.slice(-4000) : rawOutput;
|
|
29
|
+
|
|
30
|
+
let prompt = `Convert this text into a JSON object matching the schema.
|
|
31
|
+
|
|
32
|
+
## SCHEMA
|
|
33
|
+
\`\`\`json
|
|
34
|
+
${schemaStr}
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
## TEXT TO CONVERT
|
|
38
|
+
\`\`\`
|
|
39
|
+
${truncatedOutput}
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
## RULES
|
|
43
|
+
- Output ONLY the JSON object
|
|
44
|
+
- NO markdown code blocks
|
|
45
|
+
- NO explanations
|
|
46
|
+
- Start with { end with }
|
|
47
|
+
- Match ALL required fields from schema`;
|
|
48
|
+
|
|
49
|
+
if (previousError) {
|
|
50
|
+
prompt += `
|
|
51
|
+
|
|
52
|
+
## PREVIOUS ATTEMPT FAILED
|
|
53
|
+
Error: ${previousError}
|
|
54
|
+
Fix this issue in your response.`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return prompt;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Attempt to reformat non-JSON output into valid JSON
|
|
62
|
+
*
|
|
63
|
+
* STATUS: SDK NOT IMPLEMENTED - This function always throws.
|
|
64
|
+
* When SDK support is added to providers, this will work automatically.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} options
|
|
67
|
+
* @param {string} options.rawOutput - The non-JSON output to reformat
|
|
68
|
+
* @param {Object} options.schema - Target JSON schema
|
|
69
|
+
* @param {string} options.providerName - Provider name (claude, codex, gemini)
|
|
70
|
+
* @param {number} [options.maxAttempts=3] - Maximum reformatting attempts
|
|
71
|
+
* @param {Function} [options.onAttempt] - Callback for each attempt (attempt, error)
|
|
72
|
+
* @returns {Promise<Object>} The reformatted JSON object
|
|
73
|
+
* @throws {Error} Always throws - SDK not implemented
|
|
74
|
+
*/
|
|
75
|
+
function reformatOutput({
|
|
76
|
+
rawOutput,
|
|
77
|
+
schema: _schema,
|
|
78
|
+
providerName,
|
|
79
|
+
maxAttempts: _maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
80
|
+
onAttempt: _onAttempt,
|
|
81
|
+
}) {
|
|
82
|
+
// SDK not implemented - reformatting not available
|
|
83
|
+
// When SDK support is added, uncomment the implementation below
|
|
84
|
+
return Promise.reject(
|
|
85
|
+
new Error(
|
|
86
|
+
`Output reformatting not available: SDK not implemented for provider "${providerName}". ` +
|
|
87
|
+
`Agent output must be valid JSON. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// FUTURE: When SDK support is added to providers, uncomment this:
|
|
92
|
+
/*
|
|
93
|
+
const { getProvider } = require('../providers');
|
|
94
|
+
const provider = getProvider(providerName);
|
|
95
|
+
|
|
96
|
+
let lastError = null;
|
|
97
|
+
|
|
98
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
99
|
+
if (onAttempt) {
|
|
100
|
+
onAttempt(attempt, lastError);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prompt = buildReformatPrompt(rawOutput, schema, lastError);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const result = await provider.callSimple(prompt, {
|
|
107
|
+
level: 'level1',
|
|
108
|
+
maxTokens: 2000,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!result?.success) {
|
|
112
|
+
lastError = result?.error || 'API call failed';
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!result?.text) {
|
|
117
|
+
lastError = 'Empty response from reformatting model';
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parsed = extractJsonFromOutput(result.text, providerName);
|
|
122
|
+
|
|
123
|
+
if (!parsed) {
|
|
124
|
+
lastError = 'Could not extract JSON from reformatted output';
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const validationError = validateAgainstSchema(parsed, schema);
|
|
129
|
+
if (validationError) {
|
|
130
|
+
lastError = validationError;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return parsed;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
lastError = err.message;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Failed to reformat output after ${maxAttempts} attempts. Last error: ${lastError}`
|
|
142
|
+
);
|
|
143
|
+
*/
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate parsed output against JSON schema
|
|
148
|
+
*
|
|
149
|
+
* @param {Object} parsed - Parsed JSON object
|
|
150
|
+
* @param {Object} schema - JSON schema to validate against
|
|
151
|
+
* @returns {string|null} Error message if validation failed, null if valid
|
|
152
|
+
*/
|
|
153
|
+
function validateAgainstSchema(parsed, schema) {
|
|
154
|
+
const Ajv = require('ajv');
|
|
155
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
156
|
+
const validate = ajv.compile(schema);
|
|
157
|
+
const valid = validate(parsed);
|
|
158
|
+
|
|
159
|
+
if (!valid) {
|
|
160
|
+
const errors = (validate.errors || [])
|
|
161
|
+
.slice(0, 3)
|
|
162
|
+
.map((e) => `${e.instancePath || '#'} ${e.message}`)
|
|
163
|
+
.join('; ');
|
|
164
|
+
return errors || 'Schema validation failed';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
reformatOutput,
|
|
172
|
+
buildReformatPrompt,
|
|
173
|
+
validateAgainstSchema,
|
|
174
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
175
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema utilities for normalizing LLM output before validation.
|
|
3
|
+
*
|
|
4
|
+
* PROBLEM: LLMs (Claude, Gemini, Codex) via any interface (CLI, API) may return
|
|
5
|
+
* enum values that don't exactly match the schema (e.g., "simple" vs "SIMPLE").
|
|
6
|
+
*
|
|
7
|
+
* SOLUTION: Normalize enum values BEFORE validation. Provider-agnostic.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize enum values in parsed JSON to match schema definitions.
|
|
12
|
+
*
|
|
13
|
+
* Handles:
|
|
14
|
+
* - Case mismatches: "simple" → "SIMPLE"
|
|
15
|
+
* - Whitespace: " SIMPLE " → "SIMPLE"
|
|
16
|
+
* - Common variations: "bug" → "DEBUG", "fix" → "DEBUG"
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} result - Parsed JSON result from LLM
|
|
19
|
+
* @param {Object} schema - JSON schema with enum definitions
|
|
20
|
+
* @returns {Object} Normalized result (mutates and returns same object)
|
|
21
|
+
*/
|
|
22
|
+
function normalizeEnumValues(result, schema) {
|
|
23
|
+
if (!result || typeof result !== 'object' || !schema?.properties) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
28
|
+
if (propSchema.enum && typeof result[key] === 'string') {
|
|
29
|
+
let value = result[key].trim().toUpperCase();
|
|
30
|
+
|
|
31
|
+
// DETECT: Model copied the enum list instead of choosing (e.g., "TRIVIAL|SIMPLE|STANDARD")
|
|
32
|
+
if (value.includes('|')) {
|
|
33
|
+
const parts = value.split('|').map((p) => p.trim());
|
|
34
|
+
// Check if this looks like the enum list was copied verbatim
|
|
35
|
+
const matchCount = parts.filter((p) => propSchema.enum.includes(p)).length;
|
|
36
|
+
if (matchCount >= 2) {
|
|
37
|
+
// Model copied the format - pick the first valid option and warn
|
|
38
|
+
const firstValid = parts.find((p) => propSchema.enum.includes(p));
|
|
39
|
+
if (firstValid) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`⚠️ Model copied enum format instead of choosing. Field "${key}" had "${result[key]}", using "${firstValid}"`
|
|
42
|
+
);
|
|
43
|
+
value = firstValid;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Find exact match (case-insensitive)
|
|
49
|
+
const match = propSchema.enum.find((e) => e.toUpperCase() === value);
|
|
50
|
+
if (match) {
|
|
51
|
+
result[key] = match;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Common variations mapping
|
|
56
|
+
const variations = {
|
|
57
|
+
// taskType variations
|
|
58
|
+
BUG: 'DEBUG',
|
|
59
|
+
FIX: 'DEBUG',
|
|
60
|
+
BUGFIX: 'DEBUG',
|
|
61
|
+
BUG_FIX: 'DEBUG',
|
|
62
|
+
INVESTIGATE: 'DEBUG',
|
|
63
|
+
TROUBLESHOOT: 'DEBUG',
|
|
64
|
+
IMPLEMENT: 'TASK',
|
|
65
|
+
BUILD: 'TASK',
|
|
66
|
+
CREATE: 'TASK',
|
|
67
|
+
ADD: 'TASK',
|
|
68
|
+
FEATURE: 'TASK',
|
|
69
|
+
QUESTION: 'INQUIRY',
|
|
70
|
+
ASK: 'INQUIRY',
|
|
71
|
+
EXPLORE: 'INQUIRY',
|
|
72
|
+
RESEARCH: 'INQUIRY',
|
|
73
|
+
UNDERSTAND: 'INQUIRY',
|
|
74
|
+
// complexity variations
|
|
75
|
+
EASY: 'TRIVIAL',
|
|
76
|
+
BASIC: 'SIMPLE',
|
|
77
|
+
MINOR: 'SIMPLE',
|
|
78
|
+
MODERATE: 'STANDARD',
|
|
79
|
+
MEDIUM: 'STANDARD',
|
|
80
|
+
NORMAL: 'STANDARD',
|
|
81
|
+
HARD: 'STANDARD',
|
|
82
|
+
COMPLEX: 'CRITICAL',
|
|
83
|
+
RISKY: 'CRITICAL',
|
|
84
|
+
HIGH_RISK: 'CRITICAL',
|
|
85
|
+
DANGEROUS: 'CRITICAL',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (variations[value] && propSchema.enum.includes(variations[value])) {
|
|
89
|
+
result[key] = variations[value];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Recursively handle nested objects
|
|
94
|
+
if (propSchema.type === 'object' && propSchema.properties && result[key]) {
|
|
95
|
+
normalizeEnumValues(result[key], propSchema);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle arrays of objects
|
|
99
|
+
if (propSchema.type === 'array' && propSchema.items?.properties && Array.isArray(result[key])) {
|
|
100
|
+
for (const item of result[key]) {
|
|
101
|
+
normalizeEnumValues(item, propSchema.items);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
normalizeEnumValues,
|
|
111
|
+
};
|
package/src/agent-wrapper.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
const LogicEngine = require('./logic-engine');
|
|
14
14
|
const { validateAgentConfig } = require('./agent/agent-config');
|
|
15
15
|
const { loadSettings, validateModelAgainstMax } = require('../lib/settings');
|
|
16
|
+
const { normalizeProviderName } = require('../lib/provider-names');
|
|
17
|
+
const { getProvider } = require('./providers');
|
|
16
18
|
const { buildContext } = require('./agent/agent-context-builder');
|
|
17
19
|
const { findMatchingTrigger, evaluateTrigger } = require('./agent/agent-trigger-evaluator');
|
|
18
20
|
const { executeHook } = require('./agent/agent-hook-executor');
|
|
@@ -87,9 +89,12 @@ class AgentWrapper {
|
|
|
87
89
|
// TaskRunner DI - create mockSpawnFn wrapper
|
|
88
90
|
const taskRunner = options.taskRunner;
|
|
89
91
|
this.mockSpawnFn = (args, { context }) => {
|
|
92
|
+
const spec = this._resolveModelSpec();
|
|
90
93
|
return taskRunner.run(context, {
|
|
91
94
|
agentId: this.id,
|
|
92
95
|
model: this._selectModel(),
|
|
96
|
+
modelSpec: spec,
|
|
97
|
+
provider: this._resolveProvider(),
|
|
93
98
|
});
|
|
94
99
|
};
|
|
95
100
|
} else {
|
|
@@ -116,6 +121,90 @@ class AgentWrapper {
|
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
_resolveProvider() {
|
|
125
|
+
const settings = loadSettings();
|
|
126
|
+
const clusterConfig = this.cluster?.config || {};
|
|
127
|
+
|
|
128
|
+
const resolved =
|
|
129
|
+
clusterConfig.forceProvider ||
|
|
130
|
+
this.config.provider ||
|
|
131
|
+
clusterConfig.defaultProvider ||
|
|
132
|
+
settings.defaultProvider ||
|
|
133
|
+
'claude';
|
|
134
|
+
|
|
135
|
+
return normalizeProviderName(resolved) || 'claude';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_resolveModelSpec() {
|
|
139
|
+
const settings = loadSettings();
|
|
140
|
+
const providerName = this._resolveProvider();
|
|
141
|
+
const provider = getProvider(providerName);
|
|
142
|
+
const clusterConfig = this.cluster?.config || {};
|
|
143
|
+
const providerSettings = settings.providerSettings?.[providerName] || {};
|
|
144
|
+
const levelOverrides = providerSettings.levelOverrides || {};
|
|
145
|
+
const minLevel = providerSettings.minLevel;
|
|
146
|
+
const maxLevel = providerSettings.maxLevel;
|
|
147
|
+
const forcedLevel =
|
|
148
|
+
clusterConfig.forceProvider === providerName ? clusterConfig.forceLevel : null;
|
|
149
|
+
|
|
150
|
+
const applyReasoningOverride = (spec, override) => {
|
|
151
|
+
if (!override) return spec;
|
|
152
|
+
return { ...spec, reasoningEffort: override };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (this.modelConfig.type === 'rules') {
|
|
156
|
+
for (const rule of this.modelConfig.rules) {
|
|
157
|
+
if (this._matchesIterationRange(rule.iterations)) {
|
|
158
|
+
if (rule.model) {
|
|
159
|
+
return {
|
|
160
|
+
level: 'custom',
|
|
161
|
+
model: rule.model,
|
|
162
|
+
reasoningEffort: rule.reasoningEffort || this.config.reasoningEffort,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (rule.modelLevel) {
|
|
166
|
+
const level = provider.validateLevel(rule.modelLevel, minLevel, maxLevel);
|
|
167
|
+
const spec = provider.resolveModelSpec(level, levelOverrides);
|
|
168
|
+
return applyReasoningOverride(
|
|
169
|
+
{ ...spec, level },
|
|
170
|
+
rule.reasoningEffort || this.config.reasoningEffort
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Agent ${this.id}: No model rule matched iteration ${this.iteration}. ` +
|
|
178
|
+
`Add a catch-all rule like { "iterations": "all", "modelLevel": "level2" }`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.modelConfig.model) {
|
|
183
|
+
return {
|
|
184
|
+
level: 'custom',
|
|
185
|
+
model: this.modelConfig.model,
|
|
186
|
+
reasoningEffort: this.config.reasoningEffort,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (forcedLevel) {
|
|
191
|
+
const level = provider.validateLevel(forcedLevel, minLevel, maxLevel);
|
|
192
|
+
const spec = provider.resolveModelSpec(level, levelOverrides);
|
|
193
|
+
return applyReasoningOverride({ ...spec, level }, this.config.reasoningEffort);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.modelConfig.modelLevel) {
|
|
197
|
+
const level = provider.validateLevel(this.modelConfig.modelLevel, minLevel, maxLevel);
|
|
198
|
+
const spec = provider.resolveModelSpec(level, levelOverrides);
|
|
199
|
+
return applyReasoningOverride({ ...spec, level }, this.config.reasoningEffort);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const defaultLevel = providerSettings.defaultLevel || provider.getDefaultLevel();
|
|
203
|
+
const level = provider.validateLevel(defaultLevel, minLevel, maxLevel);
|
|
204
|
+
const spec = provider.resolveModelSpec(level, levelOverrides);
|
|
205
|
+
return applyReasoningOverride({ ...spec, level }, this.config.reasoningEffort);
|
|
206
|
+
}
|
|
207
|
+
|
|
119
208
|
/**
|
|
120
209
|
* Publish a message to the message bus, always including sender_model
|
|
121
210
|
* @private
|
|
@@ -126,6 +215,7 @@ class AgentWrapper {
|
|
|
126
215
|
cluster_id: this.cluster.id,
|
|
127
216
|
sender: this.id,
|
|
128
217
|
sender_model: this._selectModel(),
|
|
218
|
+
sender_provider: this._resolveProvider(),
|
|
129
219
|
});
|
|
130
220
|
}
|
|
131
221
|
|
|
@@ -145,6 +235,7 @@ class AgentWrapper {
|
|
|
145
235
|
role: this.role,
|
|
146
236
|
state: this.state,
|
|
147
237
|
model: this._selectModel(),
|
|
238
|
+
provider: this._resolveProvider(),
|
|
148
239
|
...details,
|
|
149
240
|
},
|
|
150
241
|
},
|
|
@@ -153,44 +244,21 @@ class AgentWrapper {
|
|
|
153
244
|
|
|
154
245
|
/**
|
|
155
246
|
* Select model based on current iteration and agent config
|
|
156
|
-
* Enforces maxModel
|
|
157
|
-
* @returns {string}
|
|
247
|
+
* Enforces legacy maxModel/minModel for Claude's haiku/sonnet/opus
|
|
248
|
+
* @returns {string|null}
|
|
158
249
|
* @private
|
|
159
250
|
*/
|
|
160
251
|
_selectModel() {
|
|
252
|
+
const spec = this._resolveModelSpec();
|
|
161
253
|
const settings = loadSettings();
|
|
162
254
|
const maxModel = settings.maxModel || 'sonnet';
|
|
255
|
+
const minModel = settings.minModel || null;
|
|
163
256
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// Get requested model from config
|
|
167
|
-
if (this.modelConfig.type === 'static') {
|
|
168
|
-
requestedModel = this.modelConfig.model;
|
|
169
|
-
} else if (this.modelConfig.type === 'rules') {
|
|
170
|
-
// Dynamic rules: evaluate based on iteration
|
|
171
|
-
for (const rule of this.modelConfig.rules) {
|
|
172
|
-
if (this._matchesIterationRange(rule.iterations)) {
|
|
173
|
-
requestedModel = rule.model;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// No match for rules: fail fast (config error)
|
|
179
|
-
if (!requestedModel) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
`Agent ${this.id}: No model rule matched iteration ${this.iteration}. ` +
|
|
182
|
-
`Add a catch-all rule like { "iterations": "all", "model": "sonnet" }`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// If no model specified (neither static nor rules), use maxModel as default
|
|
188
|
-
if (!requestedModel) {
|
|
189
|
-
return maxModel;
|
|
257
|
+
if (spec.model && ['opus', 'sonnet', 'haiku'].includes(spec.model)) {
|
|
258
|
+
return validateModelAgainstMax(spec.model, maxModel, minModel);
|
|
190
259
|
}
|
|
191
260
|
|
|
192
|
-
|
|
193
|
-
return validateModelAgainstMax(requestedModel, maxModel);
|
|
261
|
+
return spec.model || null;
|
|
194
262
|
}
|
|
195
263
|
|
|
196
264
|
/**
|
|
@@ -424,6 +492,7 @@ class AgentWrapper {
|
|
|
424
492
|
* Parse agent output to extract structured result data
|
|
425
493
|
* GENERIC - returns whatever structured output the agent provides
|
|
426
494
|
* Works with any agent schema (planner, validator, worker, etc.)
|
|
495
|
+
* Falls back to reformatting if extraction fails
|
|
427
496
|
* @private
|
|
428
497
|
*/
|
|
429
498
|
_parseResultOutput(output) {
|
|
@@ -464,10 +533,13 @@ class AgentWrapper {
|
|
|
464
533
|
* Get current agent state
|
|
465
534
|
*/
|
|
466
535
|
getState() {
|
|
536
|
+
const modelSpec = this._resolveModelSpec();
|
|
467
537
|
return {
|
|
468
538
|
id: this.id,
|
|
469
539
|
role: this.role,
|
|
470
540
|
model: this._selectModel(),
|
|
541
|
+
provider: this._resolveProvider(),
|
|
542
|
+
modelSpec,
|
|
471
543
|
state: this.state,
|
|
472
544
|
iteration: this.iteration,
|
|
473
545
|
maxIterations: this.maxIterations,
|