@lsctech/polaris 0.4.8 → 0.5.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/dist/autoresearch/proposal.js +70 -1
- package/dist/autoresearch/score.js +97 -0
- package/dist/cli/init.js +17 -0
- package/dist/cluster-state/store.js +35 -0
- package/dist/config/defaults.js +8 -0
- package/dist/config/validator.js +146 -0
- package/dist/loop/adapters/agent-subtask.js +19 -0
- package/dist/loop/adapters/terminal-cli.js +194 -11
- package/dist/loop/checkpoint.js +40 -0
- package/dist/loop/dispatch.js +193 -22
- package/dist/loop/orphan-recovery.js +56 -0
- package/dist/loop/parent.js +148 -21
- package/dist/loop/router/engine.js +229 -0
- package/dist/loop/router/index.js +5 -0
- package/dist/loop/router/types.js +2 -0
- package/dist/loop/worker-packet.js +16 -0
- package/dist/runtime/scheduling/child-selector.js +81 -0
- package/dist/smartdocs-engine/doctrine.js +101 -6
- package/dist/smartdocs-engine/ingest.js +18 -2
- package/dist/smartdocs-engine/migrate.js +1 -0
- package/package.json +1 -1
|
@@ -51,6 +51,36 @@ exports.FIX_ZONE_MAP = {
|
|
|
51
51
|
hint: "Medic artifacts detected — state repair was required. Review medic template heuristics and state-repair triggers.",
|
|
52
52
|
},
|
|
53
53
|
};
|
|
54
|
+
const ROUTER_FAILURE_FIX_ZONE_MAP = {
|
|
55
|
+
"quota-exhausted": {
|
|
56
|
+
artifact_type: "runtime-config",
|
|
57
|
+
hint: "Recurring provider quota exhaustion detected. Adjust quota policy and fallback order in execution.routerPolicy, and document expected quota behavior for operators.",
|
|
58
|
+
},
|
|
59
|
+
"capability-mismatch": {
|
|
60
|
+
artifact_type: "provider-role-recommendation",
|
|
61
|
+
hint: "Recurring capability mismatch detected. Update provider capability metadata and role task-type mapping, and document role expectations.",
|
|
62
|
+
},
|
|
63
|
+
"trust-too-low": {
|
|
64
|
+
artifact_type: "provider-role-recommendation",
|
|
65
|
+
hint: "Recurring trust-tier mismatch detected. Update trust policy thresholds or provider trust metadata, and document trust requirements for worker routing.",
|
|
66
|
+
},
|
|
67
|
+
"no-slot": {
|
|
68
|
+
artifact_type: "runtime-config",
|
|
69
|
+
hint: "Recurring slot exhaustion detected. Tune worker pool slot limits and provider slot caps in router policy.",
|
|
70
|
+
},
|
|
71
|
+
"cost-policy": {
|
|
72
|
+
artifact_type: "runtime-config",
|
|
73
|
+
hint: "Recurring cost policy rejection detected. Revisit max cost tier and quota policies in router constraints.",
|
|
74
|
+
},
|
|
75
|
+
"not-in-policy": {
|
|
76
|
+
artifact_type: "provider-role-recommendation",
|
|
77
|
+
hint: "Recurring provider policy mismatch detected. Align rotation/providers with role policy allowlists and update routing docs.",
|
|
78
|
+
},
|
|
79
|
+
"role-disabled": {
|
|
80
|
+
artifact_type: "runtime-config",
|
|
81
|
+
hint: "Worker role is repeatedly disabled by policy. Update providerPolicy.worker and role routing defaults.",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
54
84
|
// ──────────────────────────────────────────────
|
|
55
85
|
// Build proposals from a DiagnosisReport
|
|
56
86
|
// ──────────────────────────────────────────────
|
|
@@ -74,6 +104,24 @@ function buildProposals(report) {
|
|
|
74
104
|
fix_zone: `${entry.artifact_type}/${gateId}`,
|
|
75
105
|
});
|
|
76
106
|
}
|
|
107
|
+
const recurringRouterFailures = report.router_outcomes?.recurring_failures ?? [];
|
|
108
|
+
for (const failure of recurringRouterFailures) {
|
|
109
|
+
if (failure.occurrences < 2)
|
|
110
|
+
continue;
|
|
111
|
+
const entry = ROUTER_FAILURE_FIX_ZONE_MAP[failure.reason] ?? {
|
|
112
|
+
artifact_type: "runtime-config",
|
|
113
|
+
hint: "Recurring router failure detected. Review router policy configuration and fallback behavior.",
|
|
114
|
+
};
|
|
115
|
+
proposals.push({
|
|
116
|
+
gate_id: `router-failure:${failure.reason}`,
|
|
117
|
+
artifact_type: entry.artifact_type,
|
|
118
|
+
hint: `${entry.hint} Observed ${failure.occurrences} times across children: ${failure.child_ids.join(", ") || "unknown"}.`,
|
|
119
|
+
run_id: report.run_id,
|
|
120
|
+
evidence_run_ids: [report.run_id],
|
|
121
|
+
confidence: report.score,
|
|
122
|
+
fix_zone: `${entry.artifact_type}/router-failure-${failure.reason}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
77
125
|
return proposals;
|
|
78
126
|
}
|
|
79
127
|
// ──────────────────────────────────────────────
|
|
@@ -117,5 +165,26 @@ function validateDiagnosisReport(raw) {
|
|
|
117
165
|
throw new Error("Diagnosis report missing required number field: score");
|
|
118
166
|
if (!Array.isArray(r["diagnosis_hints"]))
|
|
119
167
|
throw new Error("Diagnosis report missing required array field: diagnosis_hints");
|
|
120
|
-
|
|
168
|
+
const routerOutcomesRaw = r["router_outcomes"];
|
|
169
|
+
const normalizedRouterOutcomes = routerOutcomesRaw && typeof routerOutcomesRaw === "object"
|
|
170
|
+
? {
|
|
171
|
+
total_decisions: typeof routerOutcomesRaw["total_decisions"] === "number" ? routerOutcomesRaw["total_decisions"] : 0,
|
|
172
|
+
exhausted_decisions: typeof routerOutcomesRaw["exhausted_decisions"] === "number" ? routerOutcomesRaw["exhausted_decisions"] : 0,
|
|
173
|
+
fallback_attempts: typeof routerOutcomesRaw["fallback_attempts"] === "number" ? routerOutcomesRaw["fallback_attempts"] : 0,
|
|
174
|
+
successful_fallbacks: typeof routerOutcomesRaw["successful_fallbacks"] === "number" ? routerOutcomesRaw["successful_fallbacks"] : 0,
|
|
175
|
+
recurring_failures: Array.isArray(routerOutcomesRaw["recurring_failures"])
|
|
176
|
+
? routerOutcomesRaw["recurring_failures"]
|
|
177
|
+
: [],
|
|
178
|
+
}
|
|
179
|
+
: {
|
|
180
|
+
total_decisions: 0,
|
|
181
|
+
exhausted_decisions: 0,
|
|
182
|
+
fallback_attempts: 0,
|
|
183
|
+
successful_fallbacks: 0,
|
|
184
|
+
recurring_failures: [],
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
...raw,
|
|
188
|
+
router_outcomes: normalizedRouterOutcomes,
|
|
189
|
+
};
|
|
121
190
|
}
|
|
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
exports.loadRunArtifacts = loadRunArtifacts;
|
|
20
20
|
exports.computeScore = computeScore;
|
|
21
21
|
exports.buildDiagnosisHints = buildDiagnosisHints;
|
|
22
|
+
exports.summarizeRouterOutcomes = summarizeRouterOutcomes;
|
|
22
23
|
exports.scoreRun = scoreRun;
|
|
23
24
|
const node_fs_1 = require("node:fs");
|
|
24
25
|
const node_path_1 = require("node:path");
|
|
@@ -203,6 +204,100 @@ function buildDiagnosisHints(failedGateNames) {
|
|
|
203
204
|
return { gate, ...h };
|
|
204
205
|
});
|
|
205
206
|
}
|
|
207
|
+
function asRecord(value) {
|
|
208
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
209
|
+
? value
|
|
210
|
+
: undefined;
|
|
211
|
+
}
|
|
212
|
+
function asStringArray(value) {
|
|
213
|
+
if (!Array.isArray(value))
|
|
214
|
+
return [];
|
|
215
|
+
return value.filter((entry) => typeof entry === "string");
|
|
216
|
+
}
|
|
217
|
+
function summarizeRouterOutcomes(artifacts) {
|
|
218
|
+
const telemetry = artifacts.telemetryEvents
|
|
219
|
+
.map((event) => asRecord(event))
|
|
220
|
+
.filter((event) => event !== undefined);
|
|
221
|
+
const childCompletionStatus = new Map();
|
|
222
|
+
for (const event of telemetry) {
|
|
223
|
+
if (event["event"] !== "child-complete" || typeof event["child_id"] !== "string")
|
|
224
|
+
continue;
|
|
225
|
+
const completionStatus = typeof event["completion_status"] === "string"
|
|
226
|
+
? event["completion_status"]
|
|
227
|
+
: "done";
|
|
228
|
+
childCompletionStatus.set(event["child_id"], completionStatus);
|
|
229
|
+
}
|
|
230
|
+
const selectedEvents = telemetry.filter((event) => event["event"] === "provider-selected");
|
|
231
|
+
const exhaustedEvents = telemetry.filter((event) => event["event"] === "provider-exhausted");
|
|
232
|
+
const fallbackEvents = telemetry.filter((event) => event["event"] === "provider-fallback-attempted");
|
|
233
|
+
const reasonCounts = new Map();
|
|
234
|
+
const countReason = (reason, childId) => {
|
|
235
|
+
const current = reasonCounts.get(reason) ?? { count: 0, childIds: new Set() };
|
|
236
|
+
current.count += 1;
|
|
237
|
+
if (childId)
|
|
238
|
+
current.childIds.add(childId);
|
|
239
|
+
reasonCounts.set(reason, current);
|
|
240
|
+
};
|
|
241
|
+
for (const event of exhaustedEvents) {
|
|
242
|
+
const reason = typeof event["reason"] === "string" ? event["reason"] : "no-provider-selected";
|
|
243
|
+
const childId = typeof event["child_id"] === "string" ? event["child_id"] : undefined;
|
|
244
|
+
countReason(reason, childId);
|
|
245
|
+
}
|
|
246
|
+
for (const event of selectedEvents) {
|
|
247
|
+
const childId = typeof event["child_id"] === "string" ? event["child_id"] : undefined;
|
|
248
|
+
const selectedProvider = typeof event["selected_provider"] === "string" ? event["selected_provider"] : null;
|
|
249
|
+
if (!selectedProvider) {
|
|
250
|
+
// Use per-candidate rejection reasons when available; fall back to the
|
|
251
|
+
// overall exhausted reason. Counting both for the same event would
|
|
252
|
+
// double-count shared reason strings (e.g. "no-slot").
|
|
253
|
+
const candidates = Array.isArray(event["router_candidates"]) ? event["router_candidates"] : [];
|
|
254
|
+
if (candidates.length > 0) {
|
|
255
|
+
for (const candidateRaw of candidates) {
|
|
256
|
+
const candidate = asRecord(candidateRaw);
|
|
257
|
+
if (!candidate)
|
|
258
|
+
continue;
|
|
259
|
+
const rejectionReasons = asStringArray(candidate["rejection_reasons"]);
|
|
260
|
+
for (const reason of rejectionReasons) {
|
|
261
|
+
countReason(reason, childId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const exhaustedReason = typeof event["router_exhausted_reason"] === "string"
|
|
267
|
+
? event["router_exhausted_reason"]
|
|
268
|
+
: "no-provider-selected";
|
|
269
|
+
countReason(exhaustedReason, childId);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let successfulFallbacks = 0;
|
|
274
|
+
for (const event of selectedEvents) {
|
|
275
|
+
const selectedProvider = typeof event["selected_provider"] === "string" ? event["selected_provider"] : null;
|
|
276
|
+
const providersTried = asStringArray(event["providers_tried"]);
|
|
277
|
+
const usedFallback = selectedProvider !== null && providersTried.length > 1;
|
|
278
|
+
if (!usedFallback)
|
|
279
|
+
continue;
|
|
280
|
+
const childId = typeof event["child_id"] === "string" ? event["child_id"] : undefined;
|
|
281
|
+
const completionStatus = childId ? childCompletionStatus.get(childId) : undefined;
|
|
282
|
+
if (completionStatus && completionStatus !== "blocked" && completionStatus !== "error") {
|
|
283
|
+
successfulFallbacks += 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const recurringFailures = Array.from(reasonCounts.entries())
|
|
287
|
+
.map(([reason, value]) => ({
|
|
288
|
+
reason,
|
|
289
|
+
occurrences: value.count,
|
|
290
|
+
child_ids: Array.from(value.childIds).sort(),
|
|
291
|
+
}))
|
|
292
|
+
.sort((a, b) => b.occurrences - a.occurrences || a.reason.localeCompare(b.reason));
|
|
293
|
+
return {
|
|
294
|
+
total_decisions: selectedEvents.length,
|
|
295
|
+
exhausted_decisions: exhaustedEvents.length,
|
|
296
|
+
fallback_attempts: fallbackEvents.length,
|
|
297
|
+
successful_fallbacks: successfulFallbacks,
|
|
298
|
+
recurring_failures: recurringFailures,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
206
301
|
// ──────────────────────────────────────────────
|
|
207
302
|
// Main entry point
|
|
208
303
|
// ──────────────────────────────────────────────
|
|
@@ -212,6 +307,7 @@ function scoreRun(repoRoot, runId) {
|
|
|
212
307
|
const failedGates = gateResults.filter((g) => g.outcome === "failed").map((g) => g.gate);
|
|
213
308
|
const score = computeScore(gateResults);
|
|
214
309
|
const diagnosisHints = buildDiagnosisHints(failedGates);
|
|
310
|
+
const routerOutcomes = summarizeRouterOutcomes(artifacts);
|
|
215
311
|
const clusterId = artifacts.currentState &&
|
|
216
312
|
typeof artifacts.currentState === "object" &&
|
|
217
313
|
!Array.isArray(artifacts.currentState)
|
|
@@ -225,5 +321,6 @@ function scoreRun(repoRoot, runId) {
|
|
|
225
321
|
failed_gates: failedGates,
|
|
226
322
|
score,
|
|
227
323
|
diagnosis_hints: diagnosisHints,
|
|
324
|
+
router_outcomes: routerOutcomes,
|
|
228
325
|
};
|
|
229
326
|
}
|
package/dist/cli/init.js
CHANGED
|
@@ -36,6 +36,13 @@ const PLAN_COMPLETE_STATUSES = new Set(["completed", "skipped"]);
|
|
|
36
36
|
const ADOPTION_LOCKED_EXECUTION = {
|
|
37
37
|
rotation: [],
|
|
38
38
|
allowCrossAgentFallback: false,
|
|
39
|
+
routerPolicy: {
|
|
40
|
+
defaultWorkerPool: {
|
|
41
|
+
maxActiveWorkers: 1,
|
|
42
|
+
maxActiveSlots: 1,
|
|
43
|
+
},
|
|
44
|
+
allowCrossProviderFallback: false,
|
|
45
|
+
},
|
|
39
46
|
adapter: "terminal-cli",
|
|
40
47
|
};
|
|
41
48
|
const ADOPTION_LOCKED_ORCHESTRATION = {
|
|
@@ -65,9 +72,19 @@ function asRecord(value) {
|
|
|
65
72
|
function isAdoptionConfigLocked(existing) {
|
|
66
73
|
const execution = asRecord(existing.execution);
|
|
67
74
|
const orchestration = asRecord(existing.orchestration);
|
|
75
|
+
const routerPolicy = asRecord(execution.routerPolicy);
|
|
76
|
+
const defaultWorkerPool = asRecord(routerPolicy.defaultWorkerPool);
|
|
77
|
+
const routerPolicyMatches = Object.keys(routerPolicy).length === 0 ||
|
|
78
|
+
(routerPolicy.allowCrossProviderFallback ===
|
|
79
|
+
ADOPTION_LOCKED_EXECUTION.routerPolicy.allowCrossProviderFallback &&
|
|
80
|
+
defaultWorkerPool.maxActiveWorkers ===
|
|
81
|
+
ADOPTION_LOCKED_EXECUTION.routerPolicy.defaultWorkerPool.maxActiveWorkers &&
|
|
82
|
+
defaultWorkerPool.maxActiveSlots ===
|
|
83
|
+
ADOPTION_LOCKED_EXECUTION.routerPolicy.defaultWorkerPool.maxActiveSlots);
|
|
68
84
|
return (Array.isArray(execution.rotation) &&
|
|
69
85
|
execution.rotation.length === 0 &&
|
|
70
86
|
execution.allowCrossAgentFallback === false &&
|
|
87
|
+
routerPolicyMatches &&
|
|
71
88
|
execution.adapter === ADOPTION_LOCKED_EXECUTION.adapter &&
|
|
72
89
|
orchestration.mode === ADOPTION_LOCKED_ORCHESTRATION.mode);
|
|
73
90
|
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.initializeClusterState = exports.writeClusterStateSync = exports.writeClusterState = exports.readClusterStateSync = exports.readClusterState = void 0;
|
|
37
|
+
exports.pruneExpiredClaims = pruneExpiredClaims;
|
|
37
38
|
const fs_1 = require("fs");
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const local_graph_1 = require("../tracker/local-graph");
|
|
@@ -44,6 +45,40 @@ const normalizeClusterState = (state) => ({
|
|
|
44
45
|
...state,
|
|
45
46
|
tracker_mutations: state.tracker_mutations ?? {},
|
|
46
47
|
});
|
|
48
|
+
function pruneExpiredClaims(state, now = new Date()) {
|
|
49
|
+
const nowMs = now.getTime();
|
|
50
|
+
const expiredChildIds = [];
|
|
51
|
+
const nextClaimMetadata = {};
|
|
52
|
+
for (const [childId, claim] of Object.entries(state.claim_metadata ?? {})) {
|
|
53
|
+
const expiresAtMs = new Date(claim.expires_at).getTime();
|
|
54
|
+
const active = Number.isFinite(expiresAtMs) && expiresAtMs > nowMs;
|
|
55
|
+
if (active) {
|
|
56
|
+
nextClaimMetadata[childId] = claim;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
expiredChildIds.push(childId);
|
|
60
|
+
}
|
|
61
|
+
if (expiredChildIds.length === 0) {
|
|
62
|
+
return { state, expiredChildIds };
|
|
63
|
+
}
|
|
64
|
+
const expiredSet = new Set(expiredChildIds);
|
|
65
|
+
const recoverableStatuses = new Set(["claimed", "dispatched", "running"]);
|
|
66
|
+
const child_states = state.child_states.map((child) => {
|
|
67
|
+
if (!expiredSet.has(child.id) || !recoverableStatuses.has(child.status)) {
|
|
68
|
+
return child;
|
|
69
|
+
}
|
|
70
|
+
return { ...child, status: "ready" };
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
state: {
|
|
74
|
+
...state,
|
|
75
|
+
state_generation: state.state_generation + 1,
|
|
76
|
+
claim_metadata: nextClaimMetadata,
|
|
77
|
+
child_states,
|
|
78
|
+
},
|
|
79
|
+
expiredChildIds,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
47
82
|
const readClusterState = async (clusterId, repoRoot) => {
|
|
48
83
|
const filePath = getClusterStatePath(clusterId, repoRoot);
|
|
49
84
|
try {
|
package/dist/config/defaults.js
CHANGED
|
@@ -39,6 +39,14 @@ exports.DEFAULT_CONFIG = {
|
|
|
39
39
|
rotation: [],
|
|
40
40
|
allowCrossAgentFallback: false,
|
|
41
41
|
roles: {},
|
|
42
|
+
routerPolicy: {
|
|
43
|
+
defaultWorkerPool: {
|
|
44
|
+
maxActiveWorkers: 1,
|
|
45
|
+
maxActiveSlots: 1,
|
|
46
|
+
},
|
|
47
|
+
providerRegistry: {},
|
|
48
|
+
allowCrossProviderFallback: false,
|
|
49
|
+
},
|
|
42
50
|
},
|
|
43
51
|
finalize: {
|
|
44
52
|
targetBranch: "main",
|
package/dist/config/validator.js
CHANGED
|
@@ -7,6 +7,9 @@ function isPlainObject(value) {
|
|
|
7
7
|
function isNumber(value) {
|
|
8
8
|
return typeof value === "number" && !Number.isNaN(value);
|
|
9
9
|
}
|
|
10
|
+
function isPositiveInteger(value) {
|
|
11
|
+
return Number.isInteger(value) && Number(value) > 0;
|
|
12
|
+
}
|
|
10
13
|
function isString(value) {
|
|
11
14
|
return typeof value === "string";
|
|
12
15
|
}
|
|
@@ -44,6 +47,29 @@ const SUPPORTED_EXECUTION_ADAPTERS = [
|
|
|
44
47
|
"remote-worker",
|
|
45
48
|
"cross-agent",
|
|
46
49
|
];
|
|
50
|
+
const SUPPORTED_ROUTER_CAPABILITIES = [
|
|
51
|
+
"orchestration",
|
|
52
|
+
"analysis",
|
|
53
|
+
"implementation",
|
|
54
|
+
"repair",
|
|
55
|
+
"docs",
|
|
56
|
+
"finalization",
|
|
57
|
+
];
|
|
58
|
+
const SUPPORTED_ROUTER_TASK_TYPES = [
|
|
59
|
+
"startup",
|
|
60
|
+
"analyze",
|
|
61
|
+
"impl",
|
|
62
|
+
"repair",
|
|
63
|
+
"docs",
|
|
64
|
+
"finalize",
|
|
65
|
+
];
|
|
66
|
+
const SUPPORTED_ROUTER_TRUST_TIERS = ["sandbox", "standard", "trusted"];
|
|
67
|
+
const SUPPORTED_ROUTER_COST_TIERS = ["low", "medium", "high"];
|
|
68
|
+
const SUPPORTED_ROUTER_QUOTA_POLICIES = [
|
|
69
|
+
"best-effort",
|
|
70
|
+
"rate-limited",
|
|
71
|
+
"reserved",
|
|
72
|
+
];
|
|
47
73
|
const SUPPORTED_LIFECYCLE_STATES = [
|
|
48
74
|
"backlog",
|
|
49
75
|
"in_progress",
|
|
@@ -325,6 +351,126 @@ function validateConfig(config) {
|
|
|
325
351
|
}
|
|
326
352
|
}
|
|
327
353
|
}
|
|
354
|
+
if ("routerPolicy" in config.execution && config.execution.routerPolicy !== undefined) {
|
|
355
|
+
if (!isPlainObject(config.execution.routerPolicy)) {
|
|
356
|
+
result.valid = false;
|
|
357
|
+
result.errors.push("execution.routerPolicy must be a plain object");
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
const routerPolicy = config.execution.routerPolicy;
|
|
361
|
+
const providerKeys = isPlainObject(config.execution.providers)
|
|
362
|
+
? new Set(Object.keys(config.execution.providers))
|
|
363
|
+
: null;
|
|
364
|
+
if ("allowCrossProviderFallback" in routerPolicy &&
|
|
365
|
+
routerPolicy.allowCrossProviderFallback !== undefined &&
|
|
366
|
+
!isBoolean(routerPolicy.allowCrossProviderFallback)) {
|
|
367
|
+
result.valid = false;
|
|
368
|
+
result.errors.push("execution.routerPolicy.allowCrossProviderFallback must be a boolean");
|
|
369
|
+
}
|
|
370
|
+
if (isBoolean(config.execution.allowCrossAgentFallback) &&
|
|
371
|
+
isBoolean(routerPolicy.allowCrossProviderFallback) &&
|
|
372
|
+
config.execution.allowCrossAgentFallback !== routerPolicy.allowCrossProviderFallback) {
|
|
373
|
+
result.valid = false;
|
|
374
|
+
result.errors.push("execution fallback policy is ambiguous: allowCrossAgentFallback conflicts with execution.routerPolicy.allowCrossProviderFallback");
|
|
375
|
+
}
|
|
376
|
+
if ("defaultWorkerPool" in routerPolicy && routerPolicy.defaultWorkerPool !== undefined) {
|
|
377
|
+
if (!isPlainObject(routerPolicy.defaultWorkerPool)) {
|
|
378
|
+
result.valid = false;
|
|
379
|
+
result.errors.push("execution.routerPolicy.defaultWorkerPool must be a plain object");
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
if ("maxActiveWorkers" in routerPolicy.defaultWorkerPool &&
|
|
383
|
+
routerPolicy.defaultWorkerPool.maxActiveWorkers !== undefined &&
|
|
384
|
+
!isPositiveInteger(routerPolicy.defaultWorkerPool.maxActiveWorkers)) {
|
|
385
|
+
result.valid = false;
|
|
386
|
+
result.errors.push("execution.routerPolicy.defaultWorkerPool.maxActiveWorkers must be a positive integer");
|
|
387
|
+
}
|
|
388
|
+
if ("maxActiveSlots" in routerPolicy.defaultWorkerPool &&
|
|
389
|
+
routerPolicy.defaultWorkerPool.maxActiveSlots !== undefined &&
|
|
390
|
+
!isPositiveInteger(routerPolicy.defaultWorkerPool.maxActiveSlots)) {
|
|
391
|
+
result.valid = false;
|
|
392
|
+
result.errors.push("execution.routerPolicy.defaultWorkerPool.maxActiveSlots must be a positive integer");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if ("providerRegistry" in routerPolicy && routerPolicy.providerRegistry !== undefined) {
|
|
397
|
+
if (!isPlainObject(routerPolicy.providerRegistry)) {
|
|
398
|
+
result.valid = false;
|
|
399
|
+
result.errors.push("execution.routerPolicy.providerRegistry must be a plain object");
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
for (const [providerName, providerPolicy] of Object.entries(routerPolicy.providerRegistry)) {
|
|
403
|
+
if (providerKeys && !providerKeys.has(providerName)) {
|
|
404
|
+
result.valid = false;
|
|
405
|
+
result.errors.push(`execution.routerPolicy.providerRegistry contains unknown provider: ${providerName}`);
|
|
406
|
+
}
|
|
407
|
+
if (!isPlainObject(providerPolicy)) {
|
|
408
|
+
result.valid = false;
|
|
409
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName} must be a plain object`);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if ("eligibleRoles" in providerPolicy && providerPolicy.eligibleRoles !== undefined) {
|
|
413
|
+
if (!Array.isArray(providerPolicy.eligibleRoles) ||
|
|
414
|
+
!providerPolicy.eligibleRoles.every((role) => isString(role) &&
|
|
415
|
+
SUPPORTED_EXECUTION_ROLES.includes(role))) {
|
|
416
|
+
result.valid = false;
|
|
417
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName}.eligibleRoles must contain only supported execution roles`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if ("capabilities" in providerPolicy && providerPolicy.capabilities !== undefined) {
|
|
421
|
+
if (!Array.isArray(providerPolicy.capabilities) ||
|
|
422
|
+
!providerPolicy.capabilities.every((capability) => isString(capability) &&
|
|
423
|
+
SUPPORTED_ROUTER_CAPABILITIES.includes(capability))) {
|
|
424
|
+
result.valid = false;
|
|
425
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName}.capabilities must contain only: orchestration, analysis, implementation, repair, docs, finalization`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if ("taskTypes" in providerPolicy && providerPolicy.taskTypes !== undefined) {
|
|
429
|
+
if (!Array.isArray(providerPolicy.taskTypes) ||
|
|
430
|
+
!providerPolicy.taskTypes.every((taskType) => isString(taskType) &&
|
|
431
|
+
SUPPORTED_ROUTER_TASK_TYPES.includes(taskType))) {
|
|
432
|
+
result.valid = false;
|
|
433
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName}.taskTypes must contain only: startup, analyze, impl, repair, docs, finalize`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if ("trustTier" in providerPolicy && providerPolicy.trustTier !== undefined) {
|
|
437
|
+
if (!isString(providerPolicy.trustTier) ||
|
|
438
|
+
!SUPPORTED_ROUTER_TRUST_TIERS.includes(providerPolicy.trustTier)) {
|
|
439
|
+
result.valid = false;
|
|
440
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName}.trustTier must be one of: sandbox, standard, trusted`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if ("costTier" in providerPolicy && providerPolicy.costTier !== undefined) {
|
|
444
|
+
if (!isString(providerPolicy.costTier) ||
|
|
445
|
+
!SUPPORTED_ROUTER_COST_TIERS.includes(providerPolicy.costTier)) {
|
|
446
|
+
result.valid = false;
|
|
447
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName}.costTier must be one of: low, medium, high`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if ("quotaPolicy" in providerPolicy && providerPolicy.quotaPolicy !== undefined) {
|
|
451
|
+
if (!isString(providerPolicy.quotaPolicy) ||
|
|
452
|
+
!SUPPORTED_ROUTER_QUOTA_POLICIES.includes(providerPolicy.quotaPolicy)) {
|
|
453
|
+
result.valid = false;
|
|
454
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName}.quotaPolicy must be one of: best-effort, rate-limited, reserved`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if ("fallbackEligible" in providerPolicy &&
|
|
458
|
+
providerPolicy.fallbackEligible !== undefined &&
|
|
459
|
+
!isBoolean(providerPolicy.fallbackEligible)) {
|
|
460
|
+
result.valid = false;
|
|
461
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName}.fallbackEligible must be a boolean`);
|
|
462
|
+
}
|
|
463
|
+
if ("maxActiveSlots" in providerPolicy &&
|
|
464
|
+
providerPolicy.maxActiveSlots !== undefined &&
|
|
465
|
+
!isPositiveInteger(providerPolicy.maxActiveSlots)) {
|
|
466
|
+
result.valid = false;
|
|
467
|
+
result.errors.push(`execution.routerPolicy.providerRegistry.${providerName}.maxActiveSlots must be a positive integer`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
328
474
|
}
|
|
329
475
|
}
|
|
330
476
|
// finalize
|
|
@@ -50,6 +50,7 @@ class AgentSubtaskAdapter {
|
|
|
50
50
|
const label = packet.active_child || ((0, worker_packet_js_1.isWorkerPacket)(packet) ? packet.worker_role : 'worker');
|
|
51
51
|
const commandRun = `agent-subtask:${label}`;
|
|
52
52
|
const provider = options.provider || "agent-subtask";
|
|
53
|
+
const routerEvidence = options.routerDecision;
|
|
53
54
|
if (!this.dispatcher) {
|
|
54
55
|
const error = "Native ephemeral agent subtask dispatch is unavailable in this host environment. " +
|
|
55
56
|
"Use manual handoff or a configured terminal-cli adapter.";
|
|
@@ -60,6 +61,10 @@ class AgentSubtaskAdapter {
|
|
|
60
61
|
command_run: commandRun,
|
|
61
62
|
summary: error,
|
|
62
63
|
stderr: error,
|
|
64
|
+
failure_origin: "provider-launch",
|
|
65
|
+
failure_category: "provider-unavailable",
|
|
66
|
+
fallback_eligible: true,
|
|
67
|
+
router_evidence: routerEvidence,
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
70
|
if ((0, worker_packet_js_1.isWorkerPacket)(packet) && packet.worker_role === 'impl') {
|
|
@@ -78,6 +83,10 @@ class AgentSubtaskAdapter {
|
|
|
78
83
|
warnings: ["empty-allowed-scope"],
|
|
79
84
|
}),
|
|
80
85
|
stderr: blockedMsg,
|
|
86
|
+
failure_origin: "provider-launch",
|
|
87
|
+
failure_category: "launch-error",
|
|
88
|
+
fallback_eligible: false,
|
|
89
|
+
router_evidence: routerEvidence,
|
|
81
90
|
};
|
|
82
91
|
}
|
|
83
92
|
}
|
|
@@ -94,6 +103,7 @@ class AgentSubtaskAdapter {
|
|
|
94
103
|
next_action: "resume-parent",
|
|
95
104
|
warnings: ["dry-run"],
|
|
96
105
|
}),
|
|
106
|
+
router_evidence: routerEvidence,
|
|
97
107
|
};
|
|
98
108
|
}
|
|
99
109
|
const instructions = (0, worker_instructions_js_1.buildWorkerInstructions)(packet);
|
|
@@ -113,6 +123,10 @@ class AgentSubtaskAdapter {
|
|
|
113
123
|
command_run: commandRun,
|
|
114
124
|
summary: validationError,
|
|
115
125
|
stderr: validationError,
|
|
126
|
+
failure_origin: "worker-execution",
|
|
127
|
+
failure_category: "worker-failure",
|
|
128
|
+
fallback_eligible: false,
|
|
129
|
+
router_evidence: routerEvidence,
|
|
116
130
|
};
|
|
117
131
|
}
|
|
118
132
|
return {
|
|
@@ -120,6 +134,7 @@ class AgentSubtaskAdapter {
|
|
|
120
134
|
provider_used: provider,
|
|
121
135
|
command_run: commandRun,
|
|
122
136
|
summary,
|
|
137
|
+
router_evidence: routerEvidence,
|
|
123
138
|
};
|
|
124
139
|
}
|
|
125
140
|
catch (err) {
|
|
@@ -130,6 +145,10 @@ class AgentSubtaskAdapter {
|
|
|
130
145
|
command_run: commandRun,
|
|
131
146
|
summary: `Native ephemeral agent subtask dispatch failed: ${msg}`,
|
|
132
147
|
stderr: msg,
|
|
148
|
+
failure_origin: "worker-execution",
|
|
149
|
+
failure_category: "worker-failure",
|
|
150
|
+
fallback_eligible: false,
|
|
151
|
+
router_evidence: routerEvidence,
|
|
133
152
|
};
|
|
134
153
|
}
|
|
135
154
|
}
|