ai-maestro 1.0.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 +11 -0
- package/LICENSE +15 -0
- package/README.md +727 -0
- package/bin/maestro.mjs +246 -0
- package/package.json +29 -0
- package/src/checkpoint.mjs +102 -0
- package/src/codex-diagnostics.mjs +682 -0
- package/src/codex-home-inspect.mjs +252 -0
- package/src/codex-home.mjs +258 -0
- package/src/commands.mjs +809 -0
- package/src/config.mjs +11 -0
- package/src/debug.mjs +164 -0
- package/src/encoding-guard.mjs +125 -0
- package/src/engines/ai-router-engine.mjs +37 -0
- package/src/engines/codex-engine.mjs +21 -0
- package/src/engines/engine.mjs +21 -0
- package/src/engines/registry.mjs +29 -0
- package/src/files.mjs +65 -0
- package/src/format.mjs +132 -0
- package/src/lock.mjs +439 -0
- package/src/memory-log.mjs +18 -0
- package/src/memory.mjs +1 -0
- package/src/orchestration/attempt-chain-runtime.mjs +627 -0
- package/src/orchestration/budget-manager.mjs +121 -0
- package/src/orchestration/capability-registry.mjs +108 -0
- package/src/orchestration/consolidator.mjs +772 -0
- package/src/orchestration/delegation-executor.mjs +262 -0
- package/src/orchestration/delegation-manager.mjs +116 -0
- package/src/orchestration/deployment-intent.mjs +36 -0
- package/src/orchestration/engine-history.mjs +110 -0
- package/src/orchestration/engine-policy.mjs +73 -0
- package/src/orchestration/engine-selector.mjs +187 -0
- package/src/orchestration/execution-context.mjs +45 -0
- package/src/orchestration/failure-classifier.mjs +136 -0
- package/src/orchestration/failure-evidence.mjs +237 -0
- package/src/orchestration/fallback-chain-lock.mjs +217 -0
- package/src/orchestration/fallback-chain-trail.mjs +218 -0
- package/src/orchestration/fallback-executor.mjs +146 -0
- package/src/orchestration/fallback-graph.mjs +106 -0
- package/src/orchestration/fallback-recommendation.mjs +73 -0
- package/src/orchestration/file-conflict-detector.mjs +126 -0
- package/src/orchestration/host-validation.mjs +241 -0
- package/src/orchestration/orchestration-loop.mjs +1971 -0
- package/src/orchestration/orchestration-runtime.mjs +1019 -0
- package/src/orchestration/orchestration-scheduler.mjs +135 -0
- package/src/orchestration/orchestration-trail.mjs +192 -0
- package/src/orchestration/preflight.mjs +212 -0
- package/src/orchestration/provider-health.mjs +566 -0
- package/src/orchestration/provider-router.mjs +352 -0
- package/src/orchestration/rc-check-adapters.mjs +817 -0
- package/src/orchestration/rc-check.mjs +544 -0
- package/src/orchestration/rc-policy.mjs +200 -0
- package/src/orchestration/runtime-gate.mjs +146 -0
- package/src/orchestration/sector-managers.mjs +215 -0
- package/src/orchestration/self-hosting-canary.mjs +231 -0
- package/src/orchestration/self-hosting-readiness.mjs +244 -0
- package/src/orchestration/spec-planner.mjs +877 -0
- package/src/orchestration/subtask-planner.mjs +176 -0
- package/src/orchestration/task-classifier.mjs +241 -0
- package/src/orchestration/verifier.mjs +1608 -0
- package/src/orchestration-commands.mjs +279 -0
- package/src/planner.mjs +38 -0
- package/src/project.mjs +1 -0
- package/src/run-diagnostics.mjs +641 -0
- package/src/runner.mjs +243 -0
- package/src/safety.mjs +116 -0
- package/src/schema.mjs +371 -0
- package/src/session-commands.mjs +521 -0
- package/src/shell.mjs +93 -0
- package/src/smart-planner.mjs +249 -0
- package/src/task-commands.mjs +1182 -0
- package/src/tasks.mjs +134 -0
- package/src/ui/commands.mjs +76 -0
- package/src/ui/events.mjs +45 -0
- package/src/ui/public/app.js +600 -0
- package/src/ui/public/index.html +88 -0
- package/src/ui/public/style.css +460 -0
- package/src/ui/server.mjs +112 -0
- package/src/ui/state.mjs +504 -0
- package/src/usage.mjs +178 -0
- package/src/workspace-diff.mjs +228 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { classifyTask } from './task-classifier.mjs';
|
|
2
|
+
import { listEngineRecords } from './capability-registry.mjs';
|
|
3
|
+
import { selectEngine } from './engine-selector.mjs';
|
|
4
|
+
import { deriveHealthTargetId, getHealthState } from './provider-health.mjs';
|
|
5
|
+
import { DEFAULT_BUDGET } from './budget-manager.mjs';
|
|
6
|
+
|
|
7
|
+
export const PROVIDER_ROUTER_ACTIONS = new Set(['switch_engine', 'needs_human', 'block', 'completed']);
|
|
8
|
+
|
|
9
|
+
const AUTO_FALLBACK_FAILURE_CLASSES = new Set([
|
|
10
|
+
'infrastructure',
|
|
11
|
+
'network',
|
|
12
|
+
'timeout',
|
|
13
|
+
'stream_interruption',
|
|
14
|
+
'rate_limit',
|
|
15
|
+
'provider_unavailable',
|
|
16
|
+
'quota',
|
|
17
|
+
'model_unavailable'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const HUMAN_FAILURE_CLASSES = new Set([
|
|
21
|
+
'authentication',
|
|
22
|
+
'engine_resolution',
|
|
23
|
+
'worker_failure',
|
|
24
|
+
'deliverable_rejection',
|
|
25
|
+
'safety_block',
|
|
26
|
+
'human_decision',
|
|
27
|
+
'unknown'
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function asArray(value) {
|
|
31
|
+
return Array.isArray(value) ? value : value == null ? [] : [value];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function asSet(value) {
|
|
35
|
+
return value instanceof Set ? value : new Set(asArray(value).map(item => String(item)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeFailureClassification(input = {}) {
|
|
39
|
+
if (typeof input === 'string') {
|
|
40
|
+
return { outcome: 'failure', failureClass: input, reason: 'provided failure class' };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...input,
|
|
44
|
+
outcome: input.outcome || (input.failureClass ? 'failure' : 'unknown'),
|
|
45
|
+
failureClass: input.failureClass || input.failure_class || null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function reject(engineRecord, reason) {
|
|
50
|
+
return {
|
|
51
|
+
engineId: engineRecord && engineRecord.id ? String(engineRecord.id) : null,
|
|
52
|
+
healthTargetId: engineRecord && engineRecord.id ? String(engineRecord.id) : null,
|
|
53
|
+
accepted: false,
|
|
54
|
+
reason
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function accept(engineRecord, healthState) {
|
|
59
|
+
const healthTargetId = deriveHealthTargetId(engineRecord);
|
|
60
|
+
return {
|
|
61
|
+
engineId: engineRecord.id,
|
|
62
|
+
healthTargetId,
|
|
63
|
+
accepted: true,
|
|
64
|
+
reason: 'eligible: enabled, unvisited, CLOSED circuit, and complete provider/model/capability metadata',
|
|
65
|
+
healthState: healthState.state
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function deadlineExpired(deadline, timestamp) {
|
|
70
|
+
if (!deadline) return false;
|
|
71
|
+
const deadlineMs = Date.parse(deadline);
|
|
72
|
+
const nowMs = Date.parse(timestamp);
|
|
73
|
+
return Number.isFinite(deadlineMs) && Number.isFinite(nowMs) && nowMs >= deadlineMs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function budgetAllowsAnotherAttempt(budgetState = {}) {
|
|
77
|
+
if (budgetState.allowed === false) {
|
|
78
|
+
return { allowed: false, reason: budgetState.reason || 'budget does not allow another attempt' };
|
|
79
|
+
}
|
|
80
|
+
const maxAttempts = Number.isInteger(budgetState.maxAttempts)
|
|
81
|
+
? budgetState.maxAttempts
|
|
82
|
+
: Number.isInteger(budgetState.maxRunsPerTask)
|
|
83
|
+
? budgetState.maxRunsPerTask
|
|
84
|
+
: Number.isInteger(budgetState.budget && budgetState.budget.maxRunsPerTask)
|
|
85
|
+
? budgetState.budget.maxRunsPerTask
|
|
86
|
+
: DEFAULT_BUDGET.maxRunsPerTask;
|
|
87
|
+
const runsSoFar = Number.isInteger(budgetState.runsSoFar) ? budgetState.runsSoFar : 0;
|
|
88
|
+
if (runsSoFar >= maxAttempts) {
|
|
89
|
+
return { allowed: false, reason: 'maxRunsPerTask (' + maxAttempts + ') reached for this task (runsSoFar=' + runsSoFar + ').' };
|
|
90
|
+
}
|
|
91
|
+
return { allowed: true, maxAttempts, runsSoFar };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function engineRecordIsComplete(engineRecord) {
|
|
95
|
+
if (!engineRecord || typeof engineRecord.id !== 'string' || engineRecord.id.trim() === '') {
|
|
96
|
+
return { ok: false, reason: 'missing engine id' };
|
|
97
|
+
}
|
|
98
|
+
if (engineRecord.enabled !== true) {
|
|
99
|
+
return { ok: false, reason: 'disabled' };
|
|
100
|
+
}
|
|
101
|
+
if (!engineRecord.provider) {
|
|
102
|
+
return { ok: false, reason: 'missing provider' };
|
|
103
|
+
}
|
|
104
|
+
if (!engineRecord.model) {
|
|
105
|
+
return { ok: false, reason: 'missing model' };
|
|
106
|
+
}
|
|
107
|
+
if (!engineRecord.capabilities || typeof engineRecord.capabilities !== 'object') {
|
|
108
|
+
return { ok: false, reason: 'missing capabilities' };
|
|
109
|
+
}
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function mapSelectionAlternatives(alternatives = []) {
|
|
114
|
+
return alternatives.map(item => ({
|
|
115
|
+
engineId: item.engine || null,
|
|
116
|
+
healthTargetId: item.engine || null,
|
|
117
|
+
accepted: false,
|
|
118
|
+
reason: item.rejectedBecause || 'rejected by capability gate'
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function selectFallbackCandidate({
|
|
123
|
+
taskClassification,
|
|
124
|
+
engines,
|
|
125
|
+
visitedEngineIds = new Set(),
|
|
126
|
+
visitedHealthTargets = new Set(),
|
|
127
|
+
allowPaid = false,
|
|
128
|
+
allowUnknownCost = true,
|
|
129
|
+
preferFree = true,
|
|
130
|
+
timestamp = new Date().toISOString(),
|
|
131
|
+
dependencies = {}
|
|
132
|
+
} = {}) {
|
|
133
|
+
const getHealth = dependencies.getHealthState || getHealthState;
|
|
134
|
+
const select = dependencies.selectEngine || selectEngine;
|
|
135
|
+
const visitedEngines = asSet(visitedEngineIds);
|
|
136
|
+
const visitedTargets = asSet(visitedHealthTargets);
|
|
137
|
+
const acceptedEngines = [];
|
|
138
|
+
const candidateAudit = [];
|
|
139
|
+
|
|
140
|
+
for (const engineRecord of engines || []) {
|
|
141
|
+
const completeness = engineRecordIsComplete(engineRecord);
|
|
142
|
+
if (!completeness.ok) {
|
|
143
|
+
candidateAudit.push(reject(engineRecord, completeness.reason));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const engineId = String(engineRecord.id);
|
|
147
|
+
const healthTargetId = deriveHealthTargetId(engineRecord);
|
|
148
|
+
if (visitedEngines.has(engineId)) {
|
|
149
|
+
candidateAudit.push(reject(engineRecord, 'visited engine'));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (visitedTargets.has(healthTargetId)) {
|
|
153
|
+
candidateAudit.push(reject(engineRecord, 'visited health target'));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const healthState = await getHealth(healthTargetId, { timestamp });
|
|
157
|
+
if (healthState.state === 'OPEN') {
|
|
158
|
+
candidateAudit.push({ ...reject(engineRecord, 'circuit OPEN'), healthState: healthState.state });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (healthState.state === 'HALF_OPEN') {
|
|
162
|
+
candidateAudit.push({ ...reject(engineRecord, 'circuit HALF_OPEN is not selected automatically in 03H2A'), healthState: healthState.state });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (healthState.state !== 'CLOSED') {
|
|
166
|
+
candidateAudit.push({ ...reject(engineRecord, 'unknown circuit state'), healthState: healthState.state || null });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
acceptedEngines.push(engineRecord);
|
|
170
|
+
candidateAudit.push(accept(engineRecord, healthState));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const selection = await select(taskClassification, { engines: acceptedEngines, allowPaid, allowUnknownCost, preferFree });
|
|
174
|
+
const selectedEngineId = selection.selectedEngine || null;
|
|
175
|
+
const rejectedBySelection = mapSelectionAlternatives(selection.alternatives || []);
|
|
176
|
+
const rejectedCandidates = [
|
|
177
|
+
...candidateAudit.filter(item => !item.accepted),
|
|
178
|
+
...rejectedBySelection
|
|
179
|
+
];
|
|
180
|
+
return {
|
|
181
|
+
selectedEngineId,
|
|
182
|
+
selectedEngine: selectedEngineId,
|
|
183
|
+
acceptedCandidates: candidateAudit.filter(item => item.accepted),
|
|
184
|
+
rejectedCandidates,
|
|
185
|
+
selection
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function planProviderFallback({
|
|
190
|
+
task,
|
|
191
|
+
taskClassification: providedTaskClassification = null,
|
|
192
|
+
failureClassification: rawFailureClassification,
|
|
193
|
+
fallbackDecisionContext: providedFallbackDecisionContext = {},
|
|
194
|
+
visitedEngineIds = [],
|
|
195
|
+
visitedHealthTargets = [],
|
|
196
|
+
budgetState = {},
|
|
197
|
+
deadline = null,
|
|
198
|
+
timestamp = new Date().toISOString(),
|
|
199
|
+
allowPaid = false,
|
|
200
|
+
allowUnknownCost = true,
|
|
201
|
+
preferFree = true,
|
|
202
|
+
dependencies = {}
|
|
203
|
+
} = {}) {
|
|
204
|
+
const reason = [];
|
|
205
|
+
const failureClassification = normalizeFailureClassification(rawFailureClassification || providedFallbackDecisionContext.failureClassification || {});
|
|
206
|
+
const failureClass = failureClassification.failureClass;
|
|
207
|
+
const taskClassification = providedTaskClassification || classifyTask(task || {});
|
|
208
|
+
const capabilityRequirements = taskClassification.requires || {};
|
|
209
|
+
const fallbackDecisionContext = {
|
|
210
|
+
...providedFallbackDecisionContext,
|
|
211
|
+
taskClassification,
|
|
212
|
+
failureClassification,
|
|
213
|
+
capabilityRequirements,
|
|
214
|
+
visitedEngineIds: [...asSet(visitedEngineIds)],
|
|
215
|
+
visitedHealthTargets: [...asSet(visitedHealthTargets)],
|
|
216
|
+
budgetState
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (failureClassification.outcome === 'success') {
|
|
220
|
+
return {
|
|
221
|
+
action: 'completed',
|
|
222
|
+
candidateEngineId: null,
|
|
223
|
+
allowed: true,
|
|
224
|
+
reason: ['previous attempt succeeded'],
|
|
225
|
+
taskClassification,
|
|
226
|
+
failureClassification,
|
|
227
|
+
capabilityRequirements,
|
|
228
|
+
fallbackDecisionContext,
|
|
229
|
+
acceptedCandidates: [],
|
|
230
|
+
rejectedCandidates: []
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (deadlineExpired(deadline, timestamp)) {
|
|
235
|
+
return {
|
|
236
|
+
action: 'block',
|
|
237
|
+
terminalReason: 'fallback_exhausted',
|
|
238
|
+
candidateEngineId: null,
|
|
239
|
+
allowed: false,
|
|
240
|
+
reason: ['global fallback deadline expired'],
|
|
241
|
+
taskClassification,
|
|
242
|
+
failureClassification,
|
|
243
|
+
capabilityRequirements,
|
|
244
|
+
fallbackDecisionContext,
|
|
245
|
+
acceptedCandidates: [],
|
|
246
|
+
rejectedCandidates: []
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const budget = budgetAllowsAnotherAttempt(budgetState);
|
|
251
|
+
if (!budget.allowed) {
|
|
252
|
+
return {
|
|
253
|
+
action: 'block',
|
|
254
|
+
terminalReason: 'fallback_blocked_by_budget',
|
|
255
|
+
candidateEngineId: null,
|
|
256
|
+
allowed: false,
|
|
257
|
+
reason: [budget.reason],
|
|
258
|
+
taskClassification,
|
|
259
|
+
failureClassification,
|
|
260
|
+
capabilityRequirements,
|
|
261
|
+
fallbackDecisionContext,
|
|
262
|
+
acceptedCandidates: [],
|
|
263
|
+
rejectedCandidates: []
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (HUMAN_FAILURE_CLASSES.has(failureClass) || !AUTO_FALLBACK_FAILURE_CLASSES.has(failureClass)) {
|
|
268
|
+
return {
|
|
269
|
+
action: 'needs_human',
|
|
270
|
+
candidateEngineId: null,
|
|
271
|
+
allowed: false,
|
|
272
|
+
reason: [failureClass === 'authentication'
|
|
273
|
+
? 'authentication requires human intervention'
|
|
274
|
+
: 'failure class is not eligible for automatic fallback in 03H2A: ' + String(failureClass || 'unknown')],
|
|
275
|
+
taskClassification,
|
|
276
|
+
failureClassification,
|
|
277
|
+
capabilityRequirements,
|
|
278
|
+
fallbackDecisionContext,
|
|
279
|
+
acceptedCandidates: [],
|
|
280
|
+
rejectedCandidates: []
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let engineRecords;
|
|
285
|
+
try {
|
|
286
|
+
const list = dependencies.listEngineRecords || listEngineRecords;
|
|
287
|
+
engineRecords = await list({ enabledOnly: false });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
action: 'needs_human',
|
|
291
|
+
candidateEngineId: null,
|
|
292
|
+
allowed: false,
|
|
293
|
+
reason: ['engine registry unreadable: ' + (error && error.message ? error.message : String(error))],
|
|
294
|
+
taskClassification,
|
|
295
|
+
failureClassification,
|
|
296
|
+
capabilityRequirements,
|
|
297
|
+
fallbackDecisionContext,
|
|
298
|
+
acceptedCandidates: [],
|
|
299
|
+
rejectedCandidates: []
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const candidate = await selectFallbackCandidate({
|
|
304
|
+
taskClassification,
|
|
305
|
+
engines: engineRecords,
|
|
306
|
+
visitedEngineIds,
|
|
307
|
+
visitedHealthTargets,
|
|
308
|
+
allowPaid,
|
|
309
|
+
allowUnknownCost,
|
|
310
|
+
preferFree,
|
|
311
|
+
timestamp,
|
|
312
|
+
dependencies
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (!candidate.selectedEngineId) {
|
|
316
|
+
reason.push('no safe compatible fallback candidate');
|
|
317
|
+
if (failureClass === 'rate_limit') {
|
|
318
|
+
reason.push('rate limit cooldown recorded; no active wait is performed');
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
action: 'needs_human',
|
|
322
|
+
candidateEngineId: null,
|
|
323
|
+
allowed: false,
|
|
324
|
+
reason,
|
|
325
|
+
taskClassification,
|
|
326
|
+
failureClassification,
|
|
327
|
+
capabilityRequirements,
|
|
328
|
+
fallbackDecisionContext,
|
|
329
|
+
acceptedCandidates: candidate.acceptedCandidates,
|
|
330
|
+
rejectedCandidates: candidate.rejectedCandidates,
|
|
331
|
+
selection: candidate.selection
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
reason.push('selected safe fallback candidate ' + candidate.selectedEngineId);
|
|
336
|
+
if (failureClass === 'rate_limit') {
|
|
337
|
+
reason.push('rate limit does not retry the same engine and does not wait actively');
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
action: 'switch_engine',
|
|
341
|
+
candidateEngineId: candidate.selectedEngineId,
|
|
342
|
+
allowed: true,
|
|
343
|
+
reason,
|
|
344
|
+
taskClassification,
|
|
345
|
+
failureClassification,
|
|
346
|
+
capabilityRequirements,
|
|
347
|
+
fallbackDecisionContext,
|
|
348
|
+
acceptedCandidates: candidate.acceptedCandidates,
|
|
349
|
+
rejectedCandidates: candidate.rejectedCandidates,
|
|
350
|
+
selection: candidate.selection
|
|
351
|
+
};
|
|
352
|
+
}
|