@harness-engineering/orchestrator 0.2.16 → 0.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/dist/index.d.mts +241 -20
- package/dist/index.d.ts +241 -20
- package/dist/index.js +1385 -327
- package/dist/index.mjs +1389 -328
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -31,12 +31,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AnalysisArchive: () => AnalysisArchive,
|
|
34
|
+
BackendRouter: () => BackendRouter,
|
|
34
35
|
ClaimManager: () => ClaimManager,
|
|
35
36
|
InteractionQueue: () => InteractionQueue,
|
|
36
37
|
LinearGraphQLStub: () => LinearGraphQLStub,
|
|
37
38
|
MockBackend: () => MockBackend,
|
|
38
39
|
ORCHESTRATOR_IDENTITY_FILE: () => ORCHESTRATOR_IDENTITY_FILE,
|
|
39
40
|
Orchestrator: () => Orchestrator,
|
|
41
|
+
OrchestratorBackendFactory: () => OrchestratorBackendFactory,
|
|
40
42
|
PRDetector: () => PRDetector,
|
|
41
43
|
PromptRenderer: () => PromptRenderer,
|
|
42
44
|
RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
|
|
@@ -49,6 +51,7 @@ __export(index_exports, {
|
|
|
49
51
|
calculateRetryDelay: () => calculateRetryDelay,
|
|
50
52
|
canDispatch: () => canDispatch,
|
|
51
53
|
computeRateLimitDelay: () => computeRateLimitDelay,
|
|
54
|
+
createBackend: () => createBackend,
|
|
52
55
|
createEmptyState: () => createEmptyState,
|
|
53
56
|
detectScopeTier: () => detectScopeTier,
|
|
54
57
|
extractHighlights: () => extractHighlights,
|
|
@@ -59,6 +62,7 @@ __export(index_exports, {
|
|
|
59
62
|
isEligible: () => isEligible,
|
|
60
63
|
launchTUI: () => launchTUI,
|
|
61
64
|
loadPublishedIndex: () => loadPublishedIndex,
|
|
65
|
+
migrateAgentConfig: () => migrateAgentConfig,
|
|
62
66
|
reconcile: () => reconcile,
|
|
63
67
|
renderAnalysisComment: () => renderAnalysisComment,
|
|
64
68
|
renderPRComment: () => renderPRComment,
|
|
@@ -1846,8 +1850,89 @@ var import_yaml = require("yaml");
|
|
|
1846
1850
|
var import_types3 = require("@harness-engineering/types");
|
|
1847
1851
|
|
|
1848
1852
|
// src/workflow/config.ts
|
|
1853
|
+
var import_zod2 = require("zod");
|
|
1849
1854
|
var import_types2 = require("@harness-engineering/types");
|
|
1855
|
+
|
|
1856
|
+
// src/workflow/schema.ts
|
|
1857
|
+
var import_zod = require("zod");
|
|
1858
|
+
var ModelSchema = import_zod.z.union([import_zod.z.string().min(1), import_zod.z.array(import_zod.z.string().min(1)).nonempty()], {
|
|
1859
|
+
errorMap: () => ({
|
|
1860
|
+
message: "model must be a non-empty string or array of strings"
|
|
1861
|
+
})
|
|
1862
|
+
});
|
|
1863
|
+
var BackendDefSchema = import_zod.z.discriminatedUnion("type", [
|
|
1864
|
+
import_zod.z.object({ type: import_zod.z.literal("mock") }).strict(),
|
|
1865
|
+
import_zod.z.object({
|
|
1866
|
+
type: import_zod.z.literal("claude"),
|
|
1867
|
+
command: import_zod.z.string().optional()
|
|
1868
|
+
}).strict(),
|
|
1869
|
+
import_zod.z.object({
|
|
1870
|
+
type: import_zod.z.literal("anthropic"),
|
|
1871
|
+
model: import_zod.z.string().min(1),
|
|
1872
|
+
apiKey: import_zod.z.string().optional()
|
|
1873
|
+
}).strict(),
|
|
1874
|
+
import_zod.z.object({
|
|
1875
|
+
type: import_zod.z.literal("openai"),
|
|
1876
|
+
model: import_zod.z.string().min(1),
|
|
1877
|
+
apiKey: import_zod.z.string().optional()
|
|
1878
|
+
}).strict(),
|
|
1879
|
+
import_zod.z.object({
|
|
1880
|
+
type: import_zod.z.literal("gemini"),
|
|
1881
|
+
model: import_zod.z.string().min(1),
|
|
1882
|
+
apiKey: import_zod.z.string().optional()
|
|
1883
|
+
}).strict(),
|
|
1884
|
+
import_zod.z.object({
|
|
1885
|
+
type: import_zod.z.literal("local"),
|
|
1886
|
+
endpoint: import_zod.z.string().url(),
|
|
1887
|
+
model: ModelSchema,
|
|
1888
|
+
apiKey: import_zod.z.string().optional(),
|
|
1889
|
+
timeoutMs: import_zod.z.number().int().positive().optional(),
|
|
1890
|
+
probeIntervalMs: import_zod.z.number().int().min(1e3).optional()
|
|
1891
|
+
}).strict(),
|
|
1892
|
+
import_zod.z.object({
|
|
1893
|
+
type: import_zod.z.literal("pi"),
|
|
1894
|
+
endpoint: import_zod.z.string().url(),
|
|
1895
|
+
model: ModelSchema,
|
|
1896
|
+
apiKey: import_zod.z.string().optional(),
|
|
1897
|
+
timeoutMs: import_zod.z.number().int().positive().optional(),
|
|
1898
|
+
probeIntervalMs: import_zod.z.number().int().min(1e3).optional()
|
|
1899
|
+
}).strict()
|
|
1900
|
+
]);
|
|
1901
|
+
var RoutingConfigSchema = import_zod.z.object({
|
|
1902
|
+
default: import_zod.z.string().min(1),
|
|
1903
|
+
"quick-fix": import_zod.z.string().optional(),
|
|
1904
|
+
"guided-change": import_zod.z.string().optional(),
|
|
1905
|
+
"full-exploration": import_zod.z.string().optional(),
|
|
1906
|
+
diagnostic: import_zod.z.string().optional(),
|
|
1907
|
+
intelligence: import_zod.z.object({
|
|
1908
|
+
sel: import_zod.z.string().optional(),
|
|
1909
|
+
pesl: import_zod.z.string().optional()
|
|
1910
|
+
}).strict().optional()
|
|
1911
|
+
}).strict();
|
|
1912
|
+
|
|
1913
|
+
// src/workflow/config.ts
|
|
1850
1914
|
var REQUIRED_SECTIONS = ["tracker", "polling", "workspace", "hooks", "agent", "server"];
|
|
1915
|
+
var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefSchema);
|
|
1916
|
+
function crossFieldRoutingIssues(backends, routing) {
|
|
1917
|
+
const issues = [];
|
|
1918
|
+
const names = new Set(Object.keys(backends));
|
|
1919
|
+
const checkRef = (path16, name) => {
|
|
1920
|
+
if (name !== void 0 && !names.has(name)) {
|
|
1921
|
+
issues.push({
|
|
1922
|
+
path: path16,
|
|
1923
|
+
message: `routing.${path16.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
checkRef(["default"], routing.default);
|
|
1928
|
+
checkRef(["quick-fix"], routing["quick-fix"]);
|
|
1929
|
+
checkRef(["guided-change"], routing["guided-change"]);
|
|
1930
|
+
checkRef(["full-exploration"], routing["full-exploration"]);
|
|
1931
|
+
checkRef(["diagnostic"], routing.diagnostic);
|
|
1932
|
+
checkRef(["intelligence", "sel"], routing.intelligence?.sel);
|
|
1933
|
+
checkRef(["intelligence", "pesl"], routing.intelligence?.pesl);
|
|
1934
|
+
return issues;
|
|
1935
|
+
}
|
|
1851
1936
|
function validateWorkflowConfig(config) {
|
|
1852
1937
|
if (!config || typeof config !== "object")
|
|
1853
1938
|
return (0, import_types2.Err)(new Error("Config is missing or not an object"));
|
|
@@ -1858,6 +1943,35 @@ function validateWorkflowConfig(config) {
|
|
|
1858
1943
|
if (c.intelligence !== void 0 && (typeof c.intelligence !== "object" || c.intelligence === null)) {
|
|
1859
1944
|
return (0, import_types2.Err)(new Error("Config intelligence section must be an object if present"));
|
|
1860
1945
|
}
|
|
1946
|
+
const agent = c.agent ?? {};
|
|
1947
|
+
const hasLegacyBackend = typeof agent.backend === "string" && agent.backend.length > 0;
|
|
1948
|
+
const hasModernBackends = agent.backends !== void 0 && typeof agent.backends === "object" && agent.backends !== null;
|
|
1949
|
+
if (!hasLegacyBackend && !hasModernBackends) {
|
|
1950
|
+
return (0, import_types2.Err)(new Error("Config must define agent.backend or agent.backends."));
|
|
1951
|
+
}
|
|
1952
|
+
if (hasModernBackends) {
|
|
1953
|
+
const backendsParsed = BackendsMapSchema.safeParse(agent.backends);
|
|
1954
|
+
if (!backendsParsed.success) {
|
|
1955
|
+
return (0, import_types2.Err)(new Error(`agent.backends: ${backendsParsed.error.message}`));
|
|
1956
|
+
}
|
|
1957
|
+
const routingParsed = RoutingConfigSchema.optional().safeParse(agent.routing);
|
|
1958
|
+
if (!routingParsed.success) {
|
|
1959
|
+
return (0, import_types2.Err)(new Error(`agent.routing: ${routingParsed.error.message}`));
|
|
1960
|
+
}
|
|
1961
|
+
if (routingParsed.data) {
|
|
1962
|
+
const cross = crossFieldRoutingIssues(
|
|
1963
|
+
backendsParsed.data,
|
|
1964
|
+
routingParsed.data
|
|
1965
|
+
);
|
|
1966
|
+
if (cross.length > 0) {
|
|
1967
|
+
return (0, import_types2.Err)(
|
|
1968
|
+
new Error(
|
|
1969
|
+
`Cross-field: ${cross.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
1970
|
+
)
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1861
1975
|
return (0, import_types2.Ok)(config);
|
|
1862
1976
|
}
|
|
1863
1977
|
function getDefaultConfig() {
|
|
@@ -2561,7 +2675,7 @@ var import_node_events = require("events");
|
|
|
2561
2675
|
var path15 = __toESM(require("path"));
|
|
2562
2676
|
var import_node_crypto7 = require("crypto");
|
|
2563
2677
|
var import_core9 = require("@harness-engineering/core");
|
|
2564
|
-
var
|
|
2678
|
+
var import_intelligence4 = require("@harness-engineering/intelligence");
|
|
2565
2679
|
var import_graph = require("@harness-engineering/graph");
|
|
2566
2680
|
|
|
2567
2681
|
// src/intelligence/pipeline-runner.ts
|
|
@@ -3242,6 +3356,428 @@ var AgentRunner = class {
|
|
|
3242
3356
|
}
|
|
3243
3357
|
};
|
|
3244
3358
|
|
|
3359
|
+
// src/agent/local-model-resolver.ts
|
|
3360
|
+
var DEFAULT_PROBE_INTERVAL_MS = 3e4;
|
|
3361
|
+
var MIN_PROBE_INTERVAL_MS = 1e3;
|
|
3362
|
+
var DEFAULT_API_KEY = "lm-studio";
|
|
3363
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
|
|
3364
|
+
var noopLogger = {
|
|
3365
|
+
info: () => void 0,
|
|
3366
|
+
warn: () => void 0
|
|
3367
|
+
};
|
|
3368
|
+
async function defaultFetchModels(endpoint, apiKey, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
3369
|
+
const url = `${endpoint.replace(/\/$/, "")}/models`;
|
|
3370
|
+
let res;
|
|
3371
|
+
try {
|
|
3372
|
+
res = await fetch(url, {
|
|
3373
|
+
headers: { Authorization: `Bearer ${apiKey ?? DEFAULT_API_KEY}` },
|
|
3374
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3375
|
+
});
|
|
3376
|
+
} catch (err) {
|
|
3377
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
3378
|
+
throw new Error(`request timeout (${timeoutMs}ms)`, { cause: err });
|
|
3379
|
+
}
|
|
3380
|
+
throw err;
|
|
3381
|
+
}
|
|
3382
|
+
if (!res.ok) {
|
|
3383
|
+
throw new Error(`probe failed: ${res.status} ${res.statusText}`);
|
|
3384
|
+
}
|
|
3385
|
+
let body;
|
|
3386
|
+
try {
|
|
3387
|
+
body = await res.json();
|
|
3388
|
+
} catch {
|
|
3389
|
+
throw new Error("malformed /v1/models response");
|
|
3390
|
+
}
|
|
3391
|
+
if (!body || typeof body !== "object" || !Array.isArray(body.data)) {
|
|
3392
|
+
throw new Error("malformed /v1/models response");
|
|
3393
|
+
}
|
|
3394
|
+
const data = body.data;
|
|
3395
|
+
const ids = [];
|
|
3396
|
+
for (const entry of data) {
|
|
3397
|
+
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
|
|
3398
|
+
throw new Error("malformed /v1/models response");
|
|
3399
|
+
}
|
|
3400
|
+
ids.push(entry.id);
|
|
3401
|
+
}
|
|
3402
|
+
return ids;
|
|
3403
|
+
}
|
|
3404
|
+
var LocalModelResolver = class {
|
|
3405
|
+
endpoint;
|
|
3406
|
+
apiKey;
|
|
3407
|
+
configured;
|
|
3408
|
+
probeIntervalMs;
|
|
3409
|
+
fetchModels;
|
|
3410
|
+
logger;
|
|
3411
|
+
timer = null;
|
|
3412
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3413
|
+
/**
|
|
3414
|
+
* Tracks an in-flight probe so concurrent invocations (interval tick while a
|
|
3415
|
+
* slow probe is running, or a manual `probe()` call mid-flight) share the
|
|
3416
|
+
* existing promise instead of racing to mutate `detected/resolved/lastError/
|
|
3417
|
+
* warnings` non-atomically across `await` points. Applies to both the timer
|
|
3418
|
+
* callback and direct `probe()` calls — any caller that arrives during an
|
|
3419
|
+
* in-flight probe gets the same promise back. Cleared in `finally` so the
|
|
3420
|
+
* next tick can start a fresh probe.
|
|
3421
|
+
*/
|
|
3422
|
+
probeInFlight = null;
|
|
3423
|
+
// Mutable status fields (composed into LocalModelStatus on demand).
|
|
3424
|
+
resolved = null;
|
|
3425
|
+
detected = [];
|
|
3426
|
+
lastProbeAt = null;
|
|
3427
|
+
lastError = null;
|
|
3428
|
+
warnings = [];
|
|
3429
|
+
available = false;
|
|
3430
|
+
constructor(opts) {
|
|
3431
|
+
this.endpoint = opts.endpoint;
|
|
3432
|
+
if (opts.apiKey !== void 0) {
|
|
3433
|
+
this.apiKey = opts.apiKey;
|
|
3434
|
+
}
|
|
3435
|
+
this.configured = [...opts.configured];
|
|
3436
|
+
const interval = opts.probeIntervalMs ?? DEFAULT_PROBE_INTERVAL_MS;
|
|
3437
|
+
this.probeIntervalMs = Math.max(MIN_PROBE_INTERVAL_MS, interval);
|
|
3438
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3439
|
+
this.fetchModels = opts.fetchModels ?? ((endpoint, apiKey) => defaultFetchModels(endpoint, apiKey, timeoutMs));
|
|
3440
|
+
this.logger = opts.logger ?? noopLogger;
|
|
3441
|
+
}
|
|
3442
|
+
resolveModel() {
|
|
3443
|
+
return this.resolved;
|
|
3444
|
+
}
|
|
3445
|
+
getStatus() {
|
|
3446
|
+
return {
|
|
3447
|
+
available: this.available,
|
|
3448
|
+
resolved: this.resolved,
|
|
3449
|
+
configured: [...this.configured],
|
|
3450
|
+
detected: [...this.detected],
|
|
3451
|
+
lastProbeAt: this.lastProbeAt,
|
|
3452
|
+
lastError: this.lastError,
|
|
3453
|
+
warnings: [...this.warnings]
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
onStatusChange(handler) {
|
|
3457
|
+
this.listeners.add(handler);
|
|
3458
|
+
return () => {
|
|
3459
|
+
this.listeners.delete(handler);
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
async probe() {
|
|
3463
|
+
if (this.probeInFlight !== null) {
|
|
3464
|
+
return this.probeInFlight;
|
|
3465
|
+
}
|
|
3466
|
+
const inFlight = this.runProbe().finally(() => {
|
|
3467
|
+
this.probeInFlight = null;
|
|
3468
|
+
});
|
|
3469
|
+
this.probeInFlight = inFlight;
|
|
3470
|
+
return inFlight;
|
|
3471
|
+
}
|
|
3472
|
+
async runProbe() {
|
|
3473
|
+
const before = this.snapshotForDiff();
|
|
3474
|
+
try {
|
|
3475
|
+
const detected = await this.fetchModels(this.endpoint, this.apiKey);
|
|
3476
|
+
this.detected = [...detected];
|
|
3477
|
+
this.lastError = null;
|
|
3478
|
+
this.lastProbeAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3479
|
+
const match = this.configured.find((id) => detected.includes(id)) ?? null;
|
|
3480
|
+
this.resolved = match;
|
|
3481
|
+
this.available = match !== null;
|
|
3482
|
+
this.warnings = match ? [] : [
|
|
3483
|
+
`No configured local model is loaded. Configured: [${this.configured.join(", ")}]. Detected: [${detected.join(", ")}].`
|
|
3484
|
+
];
|
|
3485
|
+
} catch (err) {
|
|
3486
|
+
const message = err instanceof Error ? err.message : "probe failed";
|
|
3487
|
+
this.lastError = message;
|
|
3488
|
+
this.available = false;
|
|
3489
|
+
this.resolved = null;
|
|
3490
|
+
this.warnings = [`Local model probe failed against ${this.endpoint}: ${message}.`];
|
|
3491
|
+
this.logger.warn("local-model-resolver probe failed", {
|
|
3492
|
+
endpoint: this.endpoint,
|
|
3493
|
+
error: message
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
const after = this.snapshotForDiff();
|
|
3497
|
+
const status = this.getStatus();
|
|
3498
|
+
if (before !== after) {
|
|
3499
|
+
for (const listener of this.listeners) {
|
|
3500
|
+
try {
|
|
3501
|
+
listener(status);
|
|
3502
|
+
} catch (err) {
|
|
3503
|
+
this.logger.warn("local-model-resolver listener threw", {
|
|
3504
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
return status;
|
|
3510
|
+
}
|
|
3511
|
+
async start() {
|
|
3512
|
+
if (this.timer !== null) {
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
await this.probe();
|
|
3516
|
+
this.timer = setInterval(() => {
|
|
3517
|
+
void this.probe();
|
|
3518
|
+
}, this.probeIntervalMs);
|
|
3519
|
+
const handle = this.timer;
|
|
3520
|
+
handle.unref?.();
|
|
3521
|
+
}
|
|
3522
|
+
stop() {
|
|
3523
|
+
if (this.timer !== null) {
|
|
3524
|
+
clearInterval(this.timer);
|
|
3525
|
+
this.timer = null;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
snapshotForDiff() {
|
|
3529
|
+
return JSON.stringify({
|
|
3530
|
+
available: this.available,
|
|
3531
|
+
resolved: this.resolved,
|
|
3532
|
+
configured: this.configured,
|
|
3533
|
+
detected: this.detected,
|
|
3534
|
+
lastError: this.lastError,
|
|
3535
|
+
warnings: this.warnings
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
|
|
3540
|
+
// src/agent/config-migration.ts
|
|
3541
|
+
var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
|
|
3542
|
+
function migrateAgentConfig(agent) {
|
|
3543
|
+
const warnings = [];
|
|
3544
|
+
const legacyFields = [
|
|
3545
|
+
{ path: "agent.backend", present: agent.backend !== void 0 && agent.backend !== "" },
|
|
3546
|
+
{ path: "agent.command", present: agent.command !== void 0 },
|
|
3547
|
+
{ path: "agent.model", present: agent.model !== void 0 },
|
|
3548
|
+
{ path: "agent.apiKey", present: agent.apiKey !== void 0 },
|
|
3549
|
+
{ path: "agent.localBackend", present: agent.localBackend !== void 0 },
|
|
3550
|
+
{ path: "agent.localEndpoint", present: agent.localEndpoint !== void 0 },
|
|
3551
|
+
{ path: "agent.localModel", present: agent.localModel !== void 0 },
|
|
3552
|
+
{ path: "agent.localApiKey", present: agent.localApiKey !== void 0 },
|
|
3553
|
+
{ path: "agent.localTimeoutMs", present: agent.localTimeoutMs !== void 0 },
|
|
3554
|
+
{ path: "agent.localProbeIntervalMs", present: agent.localProbeIntervalMs !== void 0 }
|
|
3555
|
+
];
|
|
3556
|
+
const presentLegacy = legacyFields.filter((f) => f.present).map((f) => f.path);
|
|
3557
|
+
const CASE1_ALWAYS_SUPPRESS = /* @__PURE__ */ new Set(["agent.backend"]);
|
|
3558
|
+
const CASE1_LOCAL_GROUP = /* @__PURE__ */ new Set([
|
|
3559
|
+
"agent.localBackend",
|
|
3560
|
+
"agent.localEndpoint",
|
|
3561
|
+
"agent.localModel",
|
|
3562
|
+
"agent.localApiKey",
|
|
3563
|
+
"agent.localTimeoutMs",
|
|
3564
|
+
"agent.localProbeIntervalMs"
|
|
3565
|
+
]);
|
|
3566
|
+
const suppressLocalGroup = agent.localBackend !== void 0;
|
|
3567
|
+
if (agent.backends !== void 0) {
|
|
3568
|
+
for (const path16 of presentLegacy) {
|
|
3569
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path16)) continue;
|
|
3570
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path16)) continue;
|
|
3571
|
+
warnings.push(
|
|
3572
|
+
`Ignoring legacy field '${path16}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3573
|
+
);
|
|
3574
|
+
}
|
|
3575
|
+
return { config: agent, warnings };
|
|
3576
|
+
}
|
|
3577
|
+
if (presentLegacy.length === 0) {
|
|
3578
|
+
return { config: agent, warnings };
|
|
3579
|
+
}
|
|
3580
|
+
const backends = {};
|
|
3581
|
+
const routing = { default: "primary" };
|
|
3582
|
+
backends.primary = synthesizePrimary(agent);
|
|
3583
|
+
if (agent.localBackend !== void 0) {
|
|
3584
|
+
backends.local = synthesizeLocal(agent);
|
|
3585
|
+
}
|
|
3586
|
+
const autoExec = agent.escalation?.autoExecute ?? [];
|
|
3587
|
+
if (backends.local !== void 0) {
|
|
3588
|
+
for (const tier of autoExec) {
|
|
3589
|
+
routing[tier] = "local";
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
for (const path16 of presentLegacy) {
|
|
3593
|
+
warnings.push(
|
|
3594
|
+
`Deprecated config field '${path16}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3595
|
+
);
|
|
3596
|
+
}
|
|
3597
|
+
return {
|
|
3598
|
+
config: {
|
|
3599
|
+
...agent,
|
|
3600
|
+
backends,
|
|
3601
|
+
routing
|
|
3602
|
+
},
|
|
3603
|
+
warnings
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
function synthesizePrimary(agent) {
|
|
3607
|
+
const backend = agent.backend;
|
|
3608
|
+
switch (backend) {
|
|
3609
|
+
case "mock":
|
|
3610
|
+
return { type: "mock" };
|
|
3611
|
+
case "claude": {
|
|
3612
|
+
const def = { type: "claude" };
|
|
3613
|
+
if (agent.command !== void 0) def.command = agent.command;
|
|
3614
|
+
return def;
|
|
3615
|
+
}
|
|
3616
|
+
case "anthropic": {
|
|
3617
|
+
if (agent.model === void 0) {
|
|
3618
|
+
throw new Error("migrateAgentConfig: agent.backend='anthropic' requires agent.model");
|
|
3619
|
+
}
|
|
3620
|
+
const def = { type: "anthropic", model: agent.model };
|
|
3621
|
+
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3622
|
+
return def;
|
|
3623
|
+
}
|
|
3624
|
+
case "openai": {
|
|
3625
|
+
if (agent.model === void 0) {
|
|
3626
|
+
throw new Error("migrateAgentConfig: agent.backend='openai' requires agent.model");
|
|
3627
|
+
}
|
|
3628
|
+
const def = { type: "openai", model: agent.model };
|
|
3629
|
+
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3630
|
+
return def;
|
|
3631
|
+
}
|
|
3632
|
+
case "gemini": {
|
|
3633
|
+
if (agent.model === void 0) {
|
|
3634
|
+
throw new Error("migrateAgentConfig: agent.backend='gemini' requires agent.model");
|
|
3635
|
+
}
|
|
3636
|
+
const def = { type: "gemini", model: agent.model };
|
|
3637
|
+
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3638
|
+
return def;
|
|
3639
|
+
}
|
|
3640
|
+
case "local": {
|
|
3641
|
+
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3642
|
+
throw new Error(
|
|
3643
|
+
"migrateAgentConfig: agent.backend='local' requires agent.localEndpoint and agent.localModel"
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
const def = {
|
|
3647
|
+
type: "local",
|
|
3648
|
+
endpoint: agent.localEndpoint,
|
|
3649
|
+
model: agent.localModel
|
|
3650
|
+
};
|
|
3651
|
+
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3652
|
+
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3653
|
+
if (agent.localProbeIntervalMs !== void 0)
|
|
3654
|
+
def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3655
|
+
return def;
|
|
3656
|
+
}
|
|
3657
|
+
case "pi": {
|
|
3658
|
+
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3659
|
+
throw new Error(
|
|
3660
|
+
"migrateAgentConfig: agent.backend='pi' requires agent.localEndpoint and agent.localModel"
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
const def = {
|
|
3664
|
+
type: "pi",
|
|
3665
|
+
endpoint: agent.localEndpoint,
|
|
3666
|
+
model: agent.localModel
|
|
3667
|
+
};
|
|
3668
|
+
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3669
|
+
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3670
|
+
if (agent.localProbeIntervalMs !== void 0)
|
|
3671
|
+
def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3672
|
+
return def;
|
|
3673
|
+
}
|
|
3674
|
+
default:
|
|
3675
|
+
throw new Error(
|
|
3676
|
+
`migrateAgentConfig: unknown legacy backend '${String(backend)}'. Expected one of: mock, claude, anthropic, openai, gemini, local, pi.`
|
|
3677
|
+
);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
function synthesizeLocal(agent) {
|
|
3681
|
+
if (agent.localBackend === void 0) {
|
|
3682
|
+
throw new Error("synthesizeLocal called without agent.localBackend");
|
|
3683
|
+
}
|
|
3684
|
+
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3685
|
+
throw new Error(
|
|
3686
|
+
"migrateAgentConfig: agent.localBackend requires agent.localEndpoint and agent.localModel"
|
|
3687
|
+
);
|
|
3688
|
+
}
|
|
3689
|
+
if (agent.localBackend === "pi") {
|
|
3690
|
+
const def2 = {
|
|
3691
|
+
type: "pi",
|
|
3692
|
+
endpoint: agent.localEndpoint,
|
|
3693
|
+
model: agent.localModel
|
|
3694
|
+
};
|
|
3695
|
+
if (agent.localApiKey !== void 0) def2.apiKey = agent.localApiKey;
|
|
3696
|
+
if (agent.localTimeoutMs !== void 0) def2.timeoutMs = agent.localTimeoutMs;
|
|
3697
|
+
if (agent.localProbeIntervalMs !== void 0) def2.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3698
|
+
return def2;
|
|
3699
|
+
}
|
|
3700
|
+
const def = {
|
|
3701
|
+
type: "local",
|
|
3702
|
+
endpoint: agent.localEndpoint,
|
|
3703
|
+
model: agent.localModel
|
|
3704
|
+
};
|
|
3705
|
+
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3706
|
+
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3707
|
+
if (agent.localProbeIntervalMs !== void 0) def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3708
|
+
return def;
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
// src/agent/backend-router.ts
|
|
3712
|
+
var BackendRouter = class {
|
|
3713
|
+
backends;
|
|
3714
|
+
routing;
|
|
3715
|
+
constructor(opts) {
|
|
3716
|
+
this.backends = opts.backends;
|
|
3717
|
+
this.routing = opts.routing;
|
|
3718
|
+
this.validateReferences();
|
|
3719
|
+
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Returns the backend name for a given use case.
|
|
3722
|
+
*
|
|
3723
|
+
* - `tier`: per-tier override, falling back to `routing.default`.
|
|
3724
|
+
* - `intelligence`: per-layer override under `routing.intelligence`,
|
|
3725
|
+
* falling back to `routing.default`.
|
|
3726
|
+
* - `maintenance` / `chat`: always `routing.default`.
|
|
3727
|
+
*/
|
|
3728
|
+
resolve(useCase) {
|
|
3729
|
+
switch (useCase.kind) {
|
|
3730
|
+
case "tier": {
|
|
3731
|
+
const named = this.routing[useCase.tier];
|
|
3732
|
+
return named ?? this.routing.default;
|
|
3733
|
+
}
|
|
3734
|
+
case "intelligence": {
|
|
3735
|
+
const intel = this.routing.intelligence;
|
|
3736
|
+
return intel?.[useCase.layer] ?? this.routing.default;
|
|
3737
|
+
}
|
|
3738
|
+
case "maintenance":
|
|
3739
|
+
case "chat":
|
|
3740
|
+
return this.routing.default;
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
/**
|
|
3744
|
+
* Returns the BackendDef reference for the resolved name. Returns the
|
|
3745
|
+
* exact reference held in `backends` (no copy) so identity comparisons
|
|
3746
|
+
* succeed (SC21).
|
|
3747
|
+
*/
|
|
3748
|
+
resolveDefinition(useCase) {
|
|
3749
|
+
const name = this.resolve(useCase);
|
|
3750
|
+
const def = this.backends[name];
|
|
3751
|
+
if (!def) {
|
|
3752
|
+
throw new Error(
|
|
3753
|
+
`BackendRouter.resolveDefinition: routing target '${name}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
return def;
|
|
3757
|
+
}
|
|
3758
|
+
validateReferences() {
|
|
3759
|
+
const known = new Set(Object.keys(this.backends));
|
|
3760
|
+
const missing = [];
|
|
3761
|
+
const check = (path16, name) => {
|
|
3762
|
+
if (name !== void 0 && !known.has(name)) missing.push({ path: path16, name });
|
|
3763
|
+
};
|
|
3764
|
+
check("default", this.routing.default);
|
|
3765
|
+
check("quick-fix", this.routing["quick-fix"]);
|
|
3766
|
+
check("guided-change", this.routing["guided-change"]);
|
|
3767
|
+
check("full-exploration", this.routing["full-exploration"]);
|
|
3768
|
+
check("diagnostic", this.routing.diagnostic);
|
|
3769
|
+
check("intelligence.sel", this.routing.intelligence?.sel);
|
|
3770
|
+
check("intelligence.pesl", this.routing.intelligence?.pesl);
|
|
3771
|
+
if (missing.length > 0) {
|
|
3772
|
+
const detail = missing.map(({ path: path16, name }) => `routing.${path16} -> '${name}'`).join("; ");
|
|
3773
|
+
const known_ = [...known].join(", ") || "(none)";
|
|
3774
|
+
throw new Error(
|
|
3775
|
+
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
};
|
|
3780
|
+
|
|
3245
3781
|
// src/agent/backends/claude.ts
|
|
3246
3782
|
var import_node_child_process4 = require("child_process");
|
|
3247
3783
|
var readline = __toESM(require("readline"));
|
|
@@ -3594,10 +4130,120 @@ var ClaudeBackend = class {
|
|
|
3594
4130
|
}
|
|
3595
4131
|
};
|
|
3596
4132
|
|
|
3597
|
-
// src/agent/backends/
|
|
3598
|
-
var
|
|
4133
|
+
// src/agent/backends/anthropic.ts
|
|
4134
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
3599
4135
|
var import_types10 = require("@harness-engineering/types");
|
|
3600
4136
|
var import_core4 = require("@harness-engineering/core");
|
|
4137
|
+
var AnthropicBackend = class {
|
|
4138
|
+
name = "anthropic";
|
|
4139
|
+
config;
|
|
4140
|
+
client;
|
|
4141
|
+
cacheAdapter;
|
|
4142
|
+
constructor(config = {}) {
|
|
4143
|
+
this.config = {
|
|
4144
|
+
model: config.model ?? "claude-sonnet-4-20250514",
|
|
4145
|
+
apiKey: config.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "",
|
|
4146
|
+
maxTokens: config.maxTokens ?? 4096
|
|
4147
|
+
};
|
|
4148
|
+
this.client = new import_sdk.default({ apiKey: this.config.apiKey });
|
|
4149
|
+
this.cacheAdapter = new import_core4.AnthropicCacheAdapter();
|
|
4150
|
+
}
|
|
4151
|
+
async startSession(params) {
|
|
4152
|
+
if (!this.config.apiKey) {
|
|
4153
|
+
return (0, import_types10.Err)({
|
|
4154
|
+
category: "agent_not_found",
|
|
4155
|
+
message: "ANTHROPIC_API_KEY is not set"
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
const session = {
|
|
4159
|
+
sessionId: `anthropic-session-${Date.now()}`,
|
|
4160
|
+
workspacePath: params.workspacePath,
|
|
4161
|
+
backendName: this.name,
|
|
4162
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4163
|
+
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4164
|
+
};
|
|
4165
|
+
return (0, import_types10.Ok)(session);
|
|
4166
|
+
}
|
|
4167
|
+
async *runTurn(session, params) {
|
|
4168
|
+
const anthropicSession = session;
|
|
4169
|
+
const systemBlocks = anthropicSession.systemPrompt ? [
|
|
4170
|
+
this.cacheAdapter.wrapSystemBlock(
|
|
4171
|
+
anthropicSession.systemPrompt,
|
|
4172
|
+
"session"
|
|
4173
|
+
)
|
|
4174
|
+
] : void 0;
|
|
4175
|
+
try {
|
|
4176
|
+
const stream = this.client.messages.stream({
|
|
4177
|
+
model: this.config.model,
|
|
4178
|
+
max_tokens: this.config.maxTokens,
|
|
4179
|
+
...systemBlocks && { system: systemBlocks },
|
|
4180
|
+
messages: [{ role: "user", content: params.prompt }]
|
|
4181
|
+
});
|
|
4182
|
+
for await (const event of stream) {
|
|
4183
|
+
if (event.type === "content_block_delta" && "text" in event.delta) {
|
|
4184
|
+
yield {
|
|
4185
|
+
type: "text",
|
|
4186
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4187
|
+
content: event.delta.text,
|
|
4188
|
+
sessionId: session.sessionId
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
const finalMessage = await stream.finalMessage();
|
|
4193
|
+
const { input_tokens, output_tokens } = finalMessage.usage;
|
|
4194
|
+
const cacheUsage = this.cacheAdapter.parseCacheUsage(finalMessage);
|
|
4195
|
+
const usage = {
|
|
4196
|
+
inputTokens: input_tokens,
|
|
4197
|
+
outputTokens: output_tokens,
|
|
4198
|
+
totalTokens: input_tokens + output_tokens,
|
|
4199
|
+
cacheCreationTokens: cacheUsage.cacheCreationTokens,
|
|
4200
|
+
cacheReadTokens: cacheUsage.cacheReadTokens
|
|
4201
|
+
};
|
|
4202
|
+
yield {
|
|
4203
|
+
type: "usage",
|
|
4204
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4205
|
+
sessionId: session.sessionId,
|
|
4206
|
+
usage
|
|
4207
|
+
};
|
|
4208
|
+
return {
|
|
4209
|
+
success: true,
|
|
4210
|
+
sessionId: session.sessionId,
|
|
4211
|
+
usage
|
|
4212
|
+
};
|
|
4213
|
+
} catch (err) {
|
|
4214
|
+
const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
|
|
4215
|
+
yield {
|
|
4216
|
+
type: "error",
|
|
4217
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4218
|
+
content: errorMessage,
|
|
4219
|
+
sessionId: session.sessionId
|
|
4220
|
+
};
|
|
4221
|
+
return {
|
|
4222
|
+
success: false,
|
|
4223
|
+
sessionId: session.sessionId,
|
|
4224
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
4225
|
+
error: errorMessage
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
async stopSession(_session) {
|
|
4230
|
+
return (0, import_types10.Ok)(void 0);
|
|
4231
|
+
}
|
|
4232
|
+
async healthCheck() {
|
|
4233
|
+
if (!this.config.apiKey) {
|
|
4234
|
+
return (0, import_types10.Err)({
|
|
4235
|
+
category: "response_error",
|
|
4236
|
+
message: "ANTHROPIC_API_KEY is not set"
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
return (0, import_types10.Ok)(void 0);
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4242
|
+
|
|
4243
|
+
// src/agent/backends/openai.ts
|
|
4244
|
+
var import_openai = __toESM(require("openai"));
|
|
4245
|
+
var import_types11 = require("@harness-engineering/types");
|
|
4246
|
+
var import_core5 = require("@harness-engineering/core");
|
|
3601
4247
|
var OpenAIBackend = class {
|
|
3602
4248
|
name = "openai";
|
|
3603
4249
|
config;
|
|
@@ -3609,11 +4255,11 @@ var OpenAIBackend = class {
|
|
|
3609
4255
|
apiKey: config.apiKey ?? process.env.OPENAI_API_KEY ?? ""
|
|
3610
4256
|
};
|
|
3611
4257
|
this.client = new import_openai.default({ apiKey: this.config.apiKey });
|
|
3612
|
-
this.cacheAdapter = new
|
|
4258
|
+
this.cacheAdapter = new import_core5.OpenAICacheAdapter();
|
|
3613
4259
|
}
|
|
3614
4260
|
async startSession(params) {
|
|
3615
4261
|
if (!this.config.apiKey) {
|
|
3616
|
-
return (0,
|
|
4262
|
+
return (0, import_types11.Err)({
|
|
3617
4263
|
category: "agent_not_found",
|
|
3618
4264
|
message: "OPENAI_API_KEY is not set"
|
|
3619
4265
|
});
|
|
@@ -3625,7 +4271,7 @@ var OpenAIBackend = class {
|
|
|
3625
4271
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3626
4272
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
3627
4273
|
};
|
|
3628
|
-
return (0,
|
|
4274
|
+
return (0, import_types11.Ok)(session);
|
|
3629
4275
|
}
|
|
3630
4276
|
async *runTurn(session, params) {
|
|
3631
4277
|
const openAISession = session;
|
|
@@ -3701,14 +4347,14 @@ var OpenAIBackend = class {
|
|
|
3701
4347
|
};
|
|
3702
4348
|
}
|
|
3703
4349
|
async stopSession(_session) {
|
|
3704
|
-
return (0,
|
|
4350
|
+
return (0, import_types11.Ok)(void 0);
|
|
3705
4351
|
}
|
|
3706
4352
|
async healthCheck() {
|
|
3707
4353
|
try {
|
|
3708
4354
|
await this.client.models.list();
|
|
3709
|
-
return (0,
|
|
4355
|
+
return (0, import_types11.Ok)(void 0);
|
|
3710
4356
|
} catch (err) {
|
|
3711
|
-
return (0,
|
|
4357
|
+
return (0, import_types11.Err)({
|
|
3712
4358
|
category: "response_error",
|
|
3713
4359
|
message: err instanceof Error ? err.message : "OpenAI health check failed"
|
|
3714
4360
|
});
|
|
@@ -3718,8 +4364,8 @@ var OpenAIBackend = class {
|
|
|
3718
4364
|
|
|
3719
4365
|
// src/agent/backends/gemini.ts
|
|
3720
4366
|
var import_generative_ai = require("@google/generative-ai");
|
|
3721
|
-
var
|
|
3722
|
-
var
|
|
4367
|
+
var import_types12 = require("@harness-engineering/types");
|
|
4368
|
+
var import_core6 = require("@harness-engineering/core");
|
|
3723
4369
|
var GeminiBackend = class {
|
|
3724
4370
|
name = "gemini";
|
|
3725
4371
|
config;
|
|
@@ -3729,11 +4375,11 @@ var GeminiBackend = class {
|
|
|
3729
4375
|
model: config.model ?? "gemini-2.0-flash",
|
|
3730
4376
|
apiKey: config.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY ?? ""
|
|
3731
4377
|
};
|
|
3732
|
-
this.cacheAdapter = new
|
|
4378
|
+
this.cacheAdapter = new import_core6.GeminiCacheAdapter();
|
|
3733
4379
|
}
|
|
3734
4380
|
async startSession(params) {
|
|
3735
4381
|
if (!this.config.apiKey) {
|
|
3736
|
-
return (0,
|
|
4382
|
+
return (0, import_types12.Err)({
|
|
3737
4383
|
category: "agent_not_found",
|
|
3738
4384
|
message: "GEMINI_API_KEY is not set"
|
|
3739
4385
|
});
|
|
@@ -3745,7 +4391,7 @@ var GeminiBackend = class {
|
|
|
3745
4391
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3746
4392
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
3747
4393
|
};
|
|
3748
|
-
return (0,
|
|
4394
|
+
return (0, import_types12.Ok)(session);
|
|
3749
4395
|
}
|
|
3750
4396
|
async *runTurn(session, params) {
|
|
3751
4397
|
const geminiSession = session;
|
|
@@ -3815,132 +4461,22 @@ var GeminiBackend = class {
|
|
|
3815
4461
|
success: true,
|
|
3816
4462
|
sessionId: session.sessionId,
|
|
3817
4463
|
usage
|
|
3818
|
-
};
|
|
3819
|
-
}
|
|
3820
|
-
async stopSession(_session) {
|
|
3821
|
-
return (0, import_types11.Ok)(void 0);
|
|
3822
|
-
}
|
|
3823
|
-
async healthCheck() {
|
|
3824
|
-
try {
|
|
3825
|
-
const genAI = new import_generative_ai.GoogleGenerativeAI(this.config.apiKey);
|
|
3826
|
-
genAI.getGenerativeModel({ model: this.config.model });
|
|
3827
|
-
return (0, import_types11.Ok)(void 0);
|
|
3828
|
-
} catch (err) {
|
|
3829
|
-
return (0, import_types11.Err)({
|
|
3830
|
-
category: "response_error",
|
|
3831
|
-
message: err instanceof Error ? err.message : "Gemini health check failed"
|
|
3832
|
-
});
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
|
-
};
|
|
3836
|
-
|
|
3837
|
-
// src/agent/backends/anthropic.ts
|
|
3838
|
-
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
3839
|
-
var import_types12 = require("@harness-engineering/types");
|
|
3840
|
-
var import_core6 = require("@harness-engineering/core");
|
|
3841
|
-
var AnthropicBackend = class {
|
|
3842
|
-
name = "anthropic";
|
|
3843
|
-
config;
|
|
3844
|
-
client;
|
|
3845
|
-
cacheAdapter;
|
|
3846
|
-
constructor(config = {}) {
|
|
3847
|
-
this.config = {
|
|
3848
|
-
model: config.model ?? "claude-sonnet-4-20250514",
|
|
3849
|
-
apiKey: config.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "",
|
|
3850
|
-
maxTokens: config.maxTokens ?? 4096
|
|
3851
|
-
};
|
|
3852
|
-
this.client = new import_sdk.default({ apiKey: this.config.apiKey });
|
|
3853
|
-
this.cacheAdapter = new import_core6.AnthropicCacheAdapter();
|
|
3854
|
-
}
|
|
3855
|
-
async startSession(params) {
|
|
3856
|
-
if (!this.config.apiKey) {
|
|
3857
|
-
return (0, import_types12.Err)({
|
|
3858
|
-
category: "agent_not_found",
|
|
3859
|
-
message: "ANTHROPIC_API_KEY is not set"
|
|
3860
|
-
});
|
|
3861
|
-
}
|
|
3862
|
-
const session = {
|
|
3863
|
-
sessionId: `anthropic-session-${Date.now()}`,
|
|
3864
|
-
workspacePath: params.workspacePath,
|
|
3865
|
-
backendName: this.name,
|
|
3866
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3867
|
-
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
3868
|
-
};
|
|
3869
|
-
return (0, import_types12.Ok)(session);
|
|
3870
|
-
}
|
|
3871
|
-
async *runTurn(session, params) {
|
|
3872
|
-
const anthropicSession = session;
|
|
3873
|
-
const systemBlocks = anthropicSession.systemPrompt ? [
|
|
3874
|
-
this.cacheAdapter.wrapSystemBlock(
|
|
3875
|
-
anthropicSession.systemPrompt,
|
|
3876
|
-
"session"
|
|
3877
|
-
)
|
|
3878
|
-
] : void 0;
|
|
3879
|
-
try {
|
|
3880
|
-
const stream = this.client.messages.stream({
|
|
3881
|
-
model: this.config.model,
|
|
3882
|
-
max_tokens: this.config.maxTokens,
|
|
3883
|
-
...systemBlocks && { system: systemBlocks },
|
|
3884
|
-
messages: [{ role: "user", content: params.prompt }]
|
|
3885
|
-
});
|
|
3886
|
-
for await (const event of stream) {
|
|
3887
|
-
if (event.type === "content_block_delta" && "text" in event.delta) {
|
|
3888
|
-
yield {
|
|
3889
|
-
type: "text",
|
|
3890
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3891
|
-
content: event.delta.text,
|
|
3892
|
-
sessionId: session.sessionId
|
|
3893
|
-
};
|
|
3894
|
-
}
|
|
3895
|
-
}
|
|
3896
|
-
const finalMessage = await stream.finalMessage();
|
|
3897
|
-
const { input_tokens, output_tokens } = finalMessage.usage;
|
|
3898
|
-
const cacheUsage = this.cacheAdapter.parseCacheUsage(finalMessage);
|
|
3899
|
-
const usage = {
|
|
3900
|
-
inputTokens: input_tokens,
|
|
3901
|
-
outputTokens: output_tokens,
|
|
3902
|
-
totalTokens: input_tokens + output_tokens,
|
|
3903
|
-
cacheCreationTokens: cacheUsage.cacheCreationTokens,
|
|
3904
|
-
cacheReadTokens: cacheUsage.cacheReadTokens
|
|
3905
|
-
};
|
|
3906
|
-
yield {
|
|
3907
|
-
type: "usage",
|
|
3908
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3909
|
-
sessionId: session.sessionId,
|
|
3910
|
-
usage
|
|
3911
|
-
};
|
|
3912
|
-
return {
|
|
3913
|
-
success: true,
|
|
3914
|
-
sessionId: session.sessionId,
|
|
3915
|
-
usage
|
|
3916
|
-
};
|
|
3917
|
-
} catch (err) {
|
|
3918
|
-
const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
|
|
3919
|
-
yield {
|
|
3920
|
-
type: "error",
|
|
3921
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3922
|
-
content: errorMessage,
|
|
3923
|
-
sessionId: session.sessionId
|
|
3924
|
-
};
|
|
3925
|
-
return {
|
|
3926
|
-
success: false,
|
|
3927
|
-
sessionId: session.sessionId,
|
|
3928
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
3929
|
-
error: errorMessage
|
|
3930
|
-
};
|
|
3931
|
-
}
|
|
4464
|
+
};
|
|
3932
4465
|
}
|
|
3933
4466
|
async stopSession(_session) {
|
|
3934
4467
|
return (0, import_types12.Ok)(void 0);
|
|
3935
4468
|
}
|
|
3936
4469
|
async healthCheck() {
|
|
3937
|
-
|
|
4470
|
+
try {
|
|
4471
|
+
const genAI = new import_generative_ai.GoogleGenerativeAI(this.config.apiKey);
|
|
4472
|
+
genAI.getGenerativeModel({ model: this.config.model });
|
|
4473
|
+
return (0, import_types12.Ok)(void 0);
|
|
4474
|
+
} catch (err) {
|
|
3938
4475
|
return (0, import_types12.Err)({
|
|
3939
4476
|
category: "response_error",
|
|
3940
|
-
message: "
|
|
4477
|
+
message: err instanceof Error ? err.message : "Gemini health check failed"
|
|
3941
4478
|
});
|
|
3942
4479
|
}
|
|
3943
|
-
return (0, import_types12.Ok)(void 0);
|
|
3944
4480
|
}
|
|
3945
4481
|
};
|
|
3946
4482
|
|
|
@@ -3951,6 +4487,7 @@ var DEFAULT_TIMEOUT_MS = 9e4;
|
|
|
3951
4487
|
var LocalBackend = class {
|
|
3952
4488
|
name = "local";
|
|
3953
4489
|
config;
|
|
4490
|
+
getModel;
|
|
3954
4491
|
client;
|
|
3955
4492
|
constructor(config = {}) {
|
|
3956
4493
|
this.config = {
|
|
@@ -3959,6 +4496,7 @@ var LocalBackend = class {
|
|
|
3959
4496
|
apiKey: config.apiKey ?? "ollama",
|
|
3960
4497
|
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
3961
4498
|
};
|
|
4499
|
+
this.getModel = config.getModel;
|
|
3962
4500
|
this.client = new import_openai2.default({
|
|
3963
4501
|
apiKey: this.config.apiKey,
|
|
3964
4502
|
baseURL: this.config.endpoint,
|
|
@@ -3966,11 +4504,25 @@ var LocalBackend = class {
|
|
|
3966
4504
|
});
|
|
3967
4505
|
}
|
|
3968
4506
|
async startSession(params) {
|
|
4507
|
+
let resolvedModel;
|
|
4508
|
+
if (this.getModel) {
|
|
4509
|
+
const candidate = this.getModel();
|
|
4510
|
+
if (candidate === null) {
|
|
4511
|
+
return (0, import_types13.Err)({
|
|
4512
|
+
category: "agent_not_found",
|
|
4513
|
+
message: "No local model available; check dashboard for details."
|
|
4514
|
+
});
|
|
4515
|
+
}
|
|
4516
|
+
resolvedModel = candidate;
|
|
4517
|
+
} else {
|
|
4518
|
+
resolvedModel = this.config.model;
|
|
4519
|
+
}
|
|
3969
4520
|
const session = {
|
|
3970
4521
|
sessionId: `local-session-${Date.now()}`,
|
|
3971
4522
|
workspacePath: params.workspacePath,
|
|
3972
4523
|
backendName: this.name,
|
|
3973
4524
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4525
|
+
resolvedModel,
|
|
3974
4526
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
3975
4527
|
};
|
|
3976
4528
|
return (0, import_types13.Ok)(session);
|
|
@@ -3987,7 +4539,7 @@ var LocalBackend = class {
|
|
|
3987
4539
|
let totalTokens = 0;
|
|
3988
4540
|
try {
|
|
3989
4541
|
const stream = await this.client.chat.completions.create({
|
|
3990
|
-
model:
|
|
4542
|
+
model: localSession.resolvedModel,
|
|
3991
4543
|
messages,
|
|
3992
4544
|
stream: true,
|
|
3993
4545
|
stream_options: { include_usage: true }
|
|
@@ -4152,13 +4704,41 @@ function buildLocalModel(config) {
|
|
|
4152
4704
|
var PiBackend = class {
|
|
4153
4705
|
name = "pi";
|
|
4154
4706
|
config;
|
|
4707
|
+
/**
|
|
4708
|
+
* Per-request timeout in ms (default 90_000). Spec 2 P2-I1: enforced at
|
|
4709
|
+
* the request boundary by `runTurn` racing `piSession.prompt()` against
|
|
4710
|
+
* an `AbortController + setTimeout(timeoutMs)`. On timeout the
|
|
4711
|
+
* underlying pi session is aborted and the turn returns a failed
|
|
4712
|
+
* `TurnResult` carrying a timeout-tagged error message. Setting
|
|
4713
|
+
* `timeoutMs: 0` disables the watchdog (preserves the pre-fix-up
|
|
4714
|
+
* "no enforcement" behavior for callers that want the SDK default).
|
|
4715
|
+
*/
|
|
4716
|
+
timeoutMs;
|
|
4155
4717
|
constructor(config = {}) {
|
|
4156
4718
|
this.config = config;
|
|
4719
|
+
this.timeoutMs = config.timeoutMs ?? 9e4;
|
|
4157
4720
|
}
|
|
4158
4721
|
async startSession(params) {
|
|
4159
4722
|
try {
|
|
4723
|
+
let resolvedModelName;
|
|
4724
|
+
if (this.config.getModel) {
|
|
4725
|
+
const candidate = this.config.getModel();
|
|
4726
|
+
if (candidate === null) {
|
|
4727
|
+
return (0, import_types14.Err)({
|
|
4728
|
+
category: "agent_not_found",
|
|
4729
|
+
message: "No local model available; check dashboard for details."
|
|
4730
|
+
});
|
|
4731
|
+
}
|
|
4732
|
+
resolvedModelName = candidate;
|
|
4733
|
+
} else {
|
|
4734
|
+
resolvedModelName = this.config.model;
|
|
4735
|
+
}
|
|
4160
4736
|
const piSdk = await import("@mariozechner/pi-coding-agent");
|
|
4161
|
-
const model = buildLocalModel(
|
|
4737
|
+
const model = buildLocalModel({
|
|
4738
|
+
model: resolvedModelName,
|
|
4739
|
+
endpoint: this.config.endpoint,
|
|
4740
|
+
apiKey: this.config.apiKey
|
|
4741
|
+
});
|
|
4162
4742
|
const { session: piSession } = await piSdk.createAgentSession({
|
|
4163
4743
|
cwd: params.workspacePath,
|
|
4164
4744
|
...model !== void 0 && { model },
|
|
@@ -4198,15 +4778,45 @@ var PiBackend = class {
|
|
|
4198
4778
|
signal();
|
|
4199
4779
|
});
|
|
4200
4780
|
session.unsubscribe = unsubscribe;
|
|
4201
|
-
|
|
4202
|
-
|
|
4781
|
+
let timeoutHandle = null;
|
|
4782
|
+
let timedOut = false;
|
|
4783
|
+
if (this.timeoutMs > 0) {
|
|
4784
|
+
timeoutHandle = setTimeout(() => {
|
|
4785
|
+
timedOut = true;
|
|
4786
|
+
promptErrorMsg = `Pi backend request timed out after ${this.timeoutMs}ms`;
|
|
4203
4787
|
promptDone = true;
|
|
4788
|
+
try {
|
|
4789
|
+
const maybeAbort = piSession.abort?.();
|
|
4790
|
+
if (maybeAbort && typeof maybeAbort.catch === "function") {
|
|
4791
|
+
maybeAbort.catch(() => {
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
} catch {
|
|
4795
|
+
}
|
|
4204
4796
|
signal();
|
|
4797
|
+
}, this.timeoutMs);
|
|
4798
|
+
}
|
|
4799
|
+
const clearTimeoutHandle = () => {
|
|
4800
|
+
if (timeoutHandle !== null) {
|
|
4801
|
+
clearTimeout(timeoutHandle);
|
|
4802
|
+
timeoutHandle = null;
|
|
4803
|
+
}
|
|
4804
|
+
};
|
|
4805
|
+
const promptPromise = piSession.prompt(params.prompt).then(
|
|
4806
|
+
() => {
|
|
4807
|
+
if (!timedOut) {
|
|
4808
|
+
clearTimeoutHandle();
|
|
4809
|
+
promptDone = true;
|
|
4810
|
+
signal();
|
|
4811
|
+
}
|
|
4205
4812
|
},
|
|
4206
4813
|
(err) => {
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4814
|
+
if (!timedOut) {
|
|
4815
|
+
clearTimeoutHandle();
|
|
4816
|
+
promptErrorMsg = err.message;
|
|
4817
|
+
promptDone = true;
|
|
4818
|
+
signal();
|
|
4819
|
+
}
|
|
4210
4820
|
}
|
|
4211
4821
|
);
|
|
4212
4822
|
let inputTokens = 0;
|
|
@@ -4222,12 +4832,15 @@ var PiBackend = class {
|
|
|
4222
4832
|
})
|
|
4223
4833
|
});
|
|
4224
4834
|
} finally {
|
|
4835
|
+
clearTimeoutHandle();
|
|
4225
4836
|
resolveWait?.();
|
|
4226
4837
|
resolveWait = null;
|
|
4227
4838
|
unsubscribe();
|
|
4228
4839
|
session.unsubscribe = null;
|
|
4229
|
-
|
|
4230
|
-
|
|
4840
|
+
if (!timedOut) {
|
|
4841
|
+
await promptPromise.catch(() => {
|
|
4842
|
+
});
|
|
4843
|
+
}
|
|
4231
4844
|
}
|
|
4232
4845
|
const totalTokens = inputTokens + outputTokens;
|
|
4233
4846
|
if (promptErrorMsg) {
|
|
@@ -4287,6 +4900,60 @@ var PiBackend = class {
|
|
|
4287
4900
|
}
|
|
4288
4901
|
};
|
|
4289
4902
|
|
|
4903
|
+
// src/agent/backend-factory.ts
|
|
4904
|
+
function makeGetModel(model) {
|
|
4905
|
+
if (typeof model === "string") return () => model;
|
|
4906
|
+
if (Array.isArray(model) && model.length > 0) return () => model[0] ?? null;
|
|
4907
|
+
return () => null;
|
|
4908
|
+
}
|
|
4909
|
+
function createBackend(def) {
|
|
4910
|
+
switch (def.type) {
|
|
4911
|
+
case "mock":
|
|
4912
|
+
return new MockBackend();
|
|
4913
|
+
case "claude":
|
|
4914
|
+
return new ClaudeBackend(def.command ?? "claude");
|
|
4915
|
+
case "anthropic":
|
|
4916
|
+
return new AnthropicBackend({
|
|
4917
|
+
model: def.model,
|
|
4918
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
|
|
4919
|
+
});
|
|
4920
|
+
case "openai":
|
|
4921
|
+
return new OpenAIBackend({
|
|
4922
|
+
model: def.model,
|
|
4923
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
|
|
4924
|
+
});
|
|
4925
|
+
case "gemini":
|
|
4926
|
+
return new GeminiBackend({
|
|
4927
|
+
model: def.model,
|
|
4928
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
|
|
4929
|
+
});
|
|
4930
|
+
case "local": {
|
|
4931
|
+
const isArray = Array.isArray(def.model);
|
|
4932
|
+
return new LocalBackend({
|
|
4933
|
+
endpoint: def.endpoint,
|
|
4934
|
+
...typeof def.model === "string" ? { model: def.model } : {},
|
|
4935
|
+
...isArray ? { getModel: makeGetModel(def.model) } : {},
|
|
4936
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
|
|
4937
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
case "pi": {
|
|
4941
|
+
const isArray = Array.isArray(def.model);
|
|
4942
|
+
return new PiBackend({
|
|
4943
|
+
endpoint: def.endpoint,
|
|
4944
|
+
...typeof def.model === "string" ? { model: def.model } : {},
|
|
4945
|
+
...isArray ? { getModel: makeGetModel(def.model) } : {},
|
|
4946
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
|
|
4947
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
default: {
|
|
4951
|
+
const exhaustive = def;
|
|
4952
|
+
throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4290
4957
|
// src/agent/backends/container.ts
|
|
4291
4958
|
var import_types15 = require("@harness-engineering/types");
|
|
4292
4959
|
function toAgentError(message, details) {
|
|
@@ -4648,6 +5315,191 @@ function createSecretBackend(config) {
|
|
|
4648
5315
|
}
|
|
4649
5316
|
}
|
|
4650
5317
|
|
|
5318
|
+
// src/agent/orchestrator-backend-factory.ts
|
|
5319
|
+
var OrchestratorBackendFactory = class {
|
|
5320
|
+
router;
|
|
5321
|
+
opts;
|
|
5322
|
+
constructor(opts) {
|
|
5323
|
+
this.opts = opts;
|
|
5324
|
+
this.router = new BackendRouter({ backends: opts.backends, routing: opts.routing });
|
|
5325
|
+
}
|
|
5326
|
+
/**
|
|
5327
|
+
* Resolve `useCase` to a backend name, materialize a fresh
|
|
5328
|
+
* `AgentBackend`, optionally rebind its model resolver, and apply
|
|
5329
|
+
* sandbox wrapping. Idempotent across calls (no caching) — the AgentRunner
|
|
5330
|
+
* holds the per-dispatch reference and discards it when the run ends.
|
|
5331
|
+
*/
|
|
5332
|
+
/**
|
|
5333
|
+
* Resolve `useCase` to its routed backend name, exposing the
|
|
5334
|
+
* router lookup without materializing a backend. Used by callers
|
|
5335
|
+
* (e.g., the orchestrator's dispatch site) that need to label
|
|
5336
|
+
* telemetry with the routed name BEFORE constructing the backend.
|
|
5337
|
+
*
|
|
5338
|
+
* Spec 2 P2-I2: previously the orchestrator labelled `LiveSession`
|
|
5339
|
+
* + `StreamRecorder` with the legacy `agent.backend` field, which
|
|
5340
|
+
* is `undefined` for pure-modern configs. Threading the routed name
|
|
5341
|
+
* through dispatch eliminates that gap.
|
|
5342
|
+
*/
|
|
5343
|
+
resolveName(useCase) {
|
|
5344
|
+
return this.router.resolve(useCase);
|
|
5345
|
+
}
|
|
5346
|
+
forUseCase(useCase) {
|
|
5347
|
+
const def = this.router.resolveDefinition(useCase);
|
|
5348
|
+
const name = this.router.resolve(useCase);
|
|
5349
|
+
let backend;
|
|
5350
|
+
if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
|
|
5351
|
+
const getModel = this.opts.getResolverModelFor(name);
|
|
5352
|
+
backend = getModel ? this.buildLocalLikeWithResolver(def, getModel) : createBackend(def);
|
|
5353
|
+
} else {
|
|
5354
|
+
backend = createBackend(def);
|
|
5355
|
+
}
|
|
5356
|
+
if (this.opts.sandboxPolicy === "docker" && this.opts.container) {
|
|
5357
|
+
backend = this.wrapInContainer(backend);
|
|
5358
|
+
}
|
|
5359
|
+
return backend;
|
|
5360
|
+
}
|
|
5361
|
+
/**
|
|
5362
|
+
* Rebuild a `local`/`pi` backend with a resolver-bound `getModel`,
|
|
5363
|
+
* mirroring `createBackend`'s local/pi branches but substituting the
|
|
5364
|
+
* head-of-array placeholder with the orchestrator-owned resolver.
|
|
5365
|
+
*/
|
|
5366
|
+
buildLocalLikeWithResolver(def, getModel) {
|
|
5367
|
+
if (def.type === "local") {
|
|
5368
|
+
return new LocalBackend({
|
|
5369
|
+
endpoint: def.endpoint,
|
|
5370
|
+
getModel,
|
|
5371
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
|
|
5372
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
5373
|
+
});
|
|
5374
|
+
}
|
|
5375
|
+
if (def.type === "pi") {
|
|
5376
|
+
return new PiBackend({
|
|
5377
|
+
endpoint: def.endpoint,
|
|
5378
|
+
getModel,
|
|
5379
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
|
|
5380
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
5381
|
+
});
|
|
5382
|
+
}
|
|
5383
|
+
throw new Error(
|
|
5384
|
+
`OrchestratorBackendFactory.buildLocalLikeWithResolver called with non-local def.type='${def.type}'`
|
|
5385
|
+
);
|
|
5386
|
+
}
|
|
5387
|
+
/**
|
|
5388
|
+
* Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret
|
|
5389
|
+
* backend per call so each dispatch sees a fresh container handle map
|
|
5390
|
+
* (ContainerBackend keeps its own per-instance Map<sessionId, handle>).
|
|
5391
|
+
*/
|
|
5392
|
+
wrapInContainer(inner) {
|
|
5393
|
+
const runtime = new DockerRuntime();
|
|
5394
|
+
const secretBackend = this.opts.secrets ? createSecretBackend(this.opts.secrets) : null;
|
|
5395
|
+
const secretKeys = this.opts.secrets?.keys ?? [];
|
|
5396
|
+
return new ContainerBackend(
|
|
5397
|
+
inner,
|
|
5398
|
+
runtime,
|
|
5399
|
+
secretBackend,
|
|
5400
|
+
this.opts.container,
|
|
5401
|
+
secretKeys
|
|
5402
|
+
);
|
|
5403
|
+
}
|
|
5404
|
+
};
|
|
5405
|
+
|
|
5406
|
+
// src/agent/analysis-provider-factory.ts
|
|
5407
|
+
var import_intelligence2 = require("@harness-engineering/intelligence");
|
|
5408
|
+
function buildAnalysisProvider(args) {
|
|
5409
|
+
const { def, backendName, layer, intelligence, logger } = args;
|
|
5410
|
+
const layerModel = layer === "sel" ? intelligence?.models?.sel : intelligence?.models?.pesl;
|
|
5411
|
+
switch (def.type) {
|
|
5412
|
+
case "local":
|
|
5413
|
+
case "pi":
|
|
5414
|
+
return buildLocalLikeProvider(def, args, layerModel);
|
|
5415
|
+
case "anthropic":
|
|
5416
|
+
return buildAnthropicProvider(def, args, layerModel);
|
|
5417
|
+
case "openai":
|
|
5418
|
+
return buildOpenAIProvider(def, args, layerModel);
|
|
5419
|
+
case "claude":
|
|
5420
|
+
return buildClaudeCliProvider(def, args, layerModel);
|
|
5421
|
+
case "mock":
|
|
5422
|
+
case "gemini":
|
|
5423
|
+
logger.warn(
|
|
5424
|
+
`Intelligence pipeline disabled for layer '${layer}': routed backend '${backendName}' has type '${def.type}' which has no AnalysisProvider implementation.`
|
|
5425
|
+
);
|
|
5426
|
+
return null;
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
function buildLocalLikeProvider(def, args, layerModel) {
|
|
5430
|
+
const { backendName, getResolverStatusSnapshot, intelligence, logger } = args;
|
|
5431
|
+
const snapshot = getResolverStatusSnapshot();
|
|
5432
|
+
if (!snapshot || !snapshot.available) {
|
|
5433
|
+
const configured = snapshot?.configured ?? [];
|
|
5434
|
+
const detected = snapshot?.detected ?? [];
|
|
5435
|
+
logger.warn(
|
|
5436
|
+
`Intelligence pipeline disabled for backend '${backendName}' at ${def.endpoint}: no configured local model loaded. Configured: [${configured.join(", ")}]. Detected: [${detected.join(", ")}].`
|
|
5437
|
+
);
|
|
5438
|
+
return null;
|
|
5439
|
+
}
|
|
5440
|
+
const model = layerModel ?? snapshot.resolved ?? void 0;
|
|
5441
|
+
const apiKey = def.apiKey ?? "ollama";
|
|
5442
|
+
logger.info(
|
|
5443
|
+
`Intelligence pipeline using backend '${backendName}' (${def.type}) at ${def.endpoint} (model: ${model ?? "(default)"})`
|
|
5444
|
+
);
|
|
5445
|
+
return new import_intelligence2.OpenAICompatibleAnalysisProvider({
|
|
5446
|
+
apiKey,
|
|
5447
|
+
baseUrl: def.endpoint,
|
|
5448
|
+
...model !== void 0 && { defaultModel: model },
|
|
5449
|
+
...intelligence?.requestTimeoutMs !== void 0 && {
|
|
5450
|
+
timeoutMs: intelligence.requestTimeoutMs
|
|
5451
|
+
},
|
|
5452
|
+
...intelligence?.promptSuffix !== void 0 && { promptSuffix: intelligence.promptSuffix },
|
|
5453
|
+
...intelligence?.jsonMode !== void 0 && { jsonMode: intelligence.jsonMode }
|
|
5454
|
+
});
|
|
5455
|
+
}
|
|
5456
|
+
function buildAnthropicProvider(def, args, layerModel) {
|
|
5457
|
+
const apiKey = def.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
5458
|
+
const model = layerModel ?? def.model;
|
|
5459
|
+
if (apiKey) {
|
|
5460
|
+
return new import_intelligence2.AnthropicAnalysisProvider({
|
|
5461
|
+
apiKey,
|
|
5462
|
+
...model !== void 0 && { defaultModel: model }
|
|
5463
|
+
});
|
|
5464
|
+
}
|
|
5465
|
+
args.logger.info(
|
|
5466
|
+
`Intelligence pipeline routed to '${args.backendName}' (anthropic) without API key \u2014 using Claude CLI fallback.`
|
|
5467
|
+
);
|
|
5468
|
+
return new import_intelligence2.ClaudeCliAnalysisProvider({
|
|
5469
|
+
...model !== void 0 && { defaultModel: model },
|
|
5470
|
+
...args.intelligence?.requestTimeoutMs !== void 0 && {
|
|
5471
|
+
timeoutMs: args.intelligence.requestTimeoutMs
|
|
5472
|
+
}
|
|
5473
|
+
});
|
|
5474
|
+
}
|
|
5475
|
+
function buildOpenAIProvider(def, args, layerModel) {
|
|
5476
|
+
const apiKey = def.apiKey ?? process.env.OPENAI_API_KEY;
|
|
5477
|
+
if (!apiKey) {
|
|
5478
|
+
args.logger.warn(
|
|
5479
|
+
`Intelligence pipeline disabled for backend '${args.backendName}' (openai): no API key configured.`
|
|
5480
|
+
);
|
|
5481
|
+
return null;
|
|
5482
|
+
}
|
|
5483
|
+
const model = layerModel ?? def.model;
|
|
5484
|
+
return new import_intelligence2.OpenAICompatibleAnalysisProvider({
|
|
5485
|
+
apiKey,
|
|
5486
|
+
baseUrl: "https://api.openai.com/v1",
|
|
5487
|
+
...model !== void 0 && { defaultModel: model },
|
|
5488
|
+
...args.intelligence?.requestTimeoutMs !== void 0 && {
|
|
5489
|
+
timeoutMs: args.intelligence.requestTimeoutMs
|
|
5490
|
+
}
|
|
5491
|
+
});
|
|
5492
|
+
}
|
|
5493
|
+
function buildClaudeCliProvider(def, args, layerModel) {
|
|
5494
|
+
return new import_intelligence2.ClaudeCliAnalysisProvider({
|
|
5495
|
+
...def.command !== void 0 && { command: def.command },
|
|
5496
|
+
...layerModel !== void 0 && { defaultModel: layerModel },
|
|
5497
|
+
...args.intelligence?.requestTimeoutMs !== void 0 && {
|
|
5498
|
+
timeoutMs: args.intelligence.requestTimeoutMs
|
|
5499
|
+
}
|
|
5500
|
+
});
|
|
5501
|
+
}
|
|
5502
|
+
|
|
4651
5503
|
// src/server/http.ts
|
|
4652
5504
|
var http = __toESM(require("http"));
|
|
4653
5505
|
var path13 = __toESM(require("path"));
|
|
@@ -4707,7 +5559,7 @@ var WebSocketBroadcaster = class {
|
|
|
4707
5559
|
};
|
|
4708
5560
|
|
|
4709
5561
|
// src/server/routes/interactions.ts
|
|
4710
|
-
var
|
|
5562
|
+
var import_zod3 = require("zod");
|
|
4711
5563
|
|
|
4712
5564
|
// src/server/utils.ts
|
|
4713
5565
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
@@ -4730,8 +5582,8 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
4730
5582
|
}
|
|
4731
5583
|
|
|
4732
5584
|
// src/server/routes/interactions.ts
|
|
4733
|
-
var InteractionUpdateSchema =
|
|
4734
|
-
status:
|
|
5585
|
+
var InteractionUpdateSchema = import_zod3.z.object({
|
|
5586
|
+
status: import_zod3.z.enum(["pending", "claimed", "resolved"])
|
|
4735
5587
|
});
|
|
4736
5588
|
var SAFE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
4737
5589
|
function sendJson(res, status, body) {
|
|
@@ -4782,12 +5634,12 @@ function handleInteractionsRoute(req, res, queue) {
|
|
|
4782
5634
|
}
|
|
4783
5635
|
|
|
4784
5636
|
// src/server/routes/plans.ts
|
|
4785
|
-
var
|
|
5637
|
+
var import_zod4 = require("zod");
|
|
4786
5638
|
var fs9 = __toESM(require("fs/promises"));
|
|
4787
5639
|
var path9 = __toESM(require("path"));
|
|
4788
|
-
var PlanWriteSchema =
|
|
4789
|
-
filename:
|
|
4790
|
-
content:
|
|
5640
|
+
var PlanWriteSchema = import_zod4.z.object({
|
|
5641
|
+
filename: import_zod4.z.string().min(1),
|
|
5642
|
+
content: import_zod4.z.string().min(1)
|
|
4791
5643
|
});
|
|
4792
5644
|
function handlePlansRoute(req, res, plansDir) {
|
|
4793
5645
|
const { method, url } = req;
|
|
@@ -4831,7 +5683,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
4831
5683
|
var import_node_child_process8 = require("child_process");
|
|
4832
5684
|
var import_node_crypto5 = require("crypto");
|
|
4833
5685
|
var readline2 = __toESM(require("readline"));
|
|
4834
|
-
var
|
|
5686
|
+
var import_zod5 = require("zod");
|
|
4835
5687
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4836
5688
|
var SAFE_ENV_PREFIXES = [
|
|
4837
5689
|
"PATH",
|
|
@@ -4866,10 +5718,10 @@ function buildChildEnv() {
|
|
|
4866
5718
|
}
|
|
4867
5719
|
return env;
|
|
4868
5720
|
}
|
|
4869
|
-
var ChatRequestSchema =
|
|
4870
|
-
prompt:
|
|
4871
|
-
system:
|
|
4872
|
-
sessionId:
|
|
5721
|
+
var ChatRequestSchema = import_zod5.z.object({
|
|
5722
|
+
prompt: import_zod5.z.string().min(1),
|
|
5723
|
+
system: import_zod5.z.string().optional(),
|
|
5724
|
+
sessionId: import_zod5.z.string().regex(UUID_RE).optional()
|
|
4873
5725
|
});
|
|
4874
5726
|
function handleChatProxyRoute(req, res, command = "claude") {
|
|
4875
5727
|
const { method, url } = req;
|
|
@@ -5078,12 +5930,12 @@ function extractChunks(event) {
|
|
|
5078
5930
|
}
|
|
5079
5931
|
|
|
5080
5932
|
// src/server/routes/analyze.ts
|
|
5081
|
-
var
|
|
5082
|
-
var
|
|
5083
|
-
var AnalyzeRequestSchema =
|
|
5084
|
-
title:
|
|
5085
|
-
description:
|
|
5086
|
-
labels:
|
|
5933
|
+
var import_intelligence3 = require("@harness-engineering/intelligence");
|
|
5934
|
+
var import_zod6 = require("zod");
|
|
5935
|
+
var AnalyzeRequestSchema = import_zod6.z.object({
|
|
5936
|
+
title: import_zod6.z.string().min(1),
|
|
5937
|
+
description: import_zod6.z.string().optional(),
|
|
5938
|
+
labels: import_zod6.z.array(import_zod6.z.string()).optional()
|
|
5087
5939
|
});
|
|
5088
5940
|
function emit2(res, event) {
|
|
5089
5941
|
res.write(`data: ${JSON.stringify(event)}
|
|
@@ -5108,7 +5960,7 @@ async function runPipeline(res, pipeline, parsed) {
|
|
|
5108
5960
|
disconnected = true;
|
|
5109
5961
|
});
|
|
5110
5962
|
emit2(res, { type: "status", text: "Converting to work item..." });
|
|
5111
|
-
const rawItem = (0,
|
|
5963
|
+
const rawItem = (0, import_intelligence3.manualToRawWorkItem)({
|
|
5112
5964
|
title: parsed.title,
|
|
5113
5965
|
description: parsed.description ?? "",
|
|
5114
5966
|
labels: parsed.labels ?? []
|
|
@@ -5151,7 +6003,7 @@ async function runPipeline(res, pipeline, parsed) {
|
|
|
5151
6003
|
}
|
|
5152
6004
|
}
|
|
5153
6005
|
if (disconnected) return;
|
|
5154
|
-
const signals = (0,
|
|
6006
|
+
const signals = (0, import_intelligence3.scoreToConcernSignals)(score);
|
|
5155
6007
|
if (signals.length > 0) {
|
|
5156
6008
|
emit2(res, { type: "signals", data: signals });
|
|
5157
6009
|
}
|
|
@@ -5191,19 +6043,19 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
5191
6043
|
// src/server/routes/roadmap-actions.ts
|
|
5192
6044
|
var fs10 = __toESM(require("fs/promises"));
|
|
5193
6045
|
var import_core7 = require("@harness-engineering/core");
|
|
5194
|
-
var
|
|
5195
|
-
var AppendRoadmapRequestSchema =
|
|
5196
|
-
title:
|
|
5197
|
-
summary:
|
|
5198
|
-
labels:
|
|
5199
|
-
enrichedSpec:
|
|
5200
|
-
intent:
|
|
5201
|
-
unknowns:
|
|
5202
|
-
ambiguities:
|
|
5203
|
-
riskSignals:
|
|
5204
|
-
affectedSystems:
|
|
6046
|
+
var import_zod7 = require("zod");
|
|
6047
|
+
var AppendRoadmapRequestSchema = import_zod7.z.object({
|
|
6048
|
+
title: import_zod7.z.string().min(1),
|
|
6049
|
+
summary: import_zod7.z.string().optional(),
|
|
6050
|
+
labels: import_zod7.z.array(import_zod7.z.string()).optional(),
|
|
6051
|
+
enrichedSpec: import_zod7.z.object({
|
|
6052
|
+
intent: import_zod7.z.string(),
|
|
6053
|
+
unknowns: import_zod7.z.array(import_zod7.z.string()),
|
|
6054
|
+
ambiguities: import_zod7.z.array(import_zod7.z.string()),
|
|
6055
|
+
riskSignals: import_zod7.z.array(import_zod7.z.string()),
|
|
6056
|
+
affectedSystems: import_zod7.z.array(import_zod7.z.object({ name: import_zod7.z.string() }))
|
|
5205
6057
|
}).optional(),
|
|
5206
|
-
cmlRecommendedRoute:
|
|
6058
|
+
cmlRecommendedRoute: import_zod7.z.enum(["local", "human", "simulation-required"]).optional()
|
|
5207
6059
|
});
|
|
5208
6060
|
function sendJSON(res, status, body) {
|
|
5209
6061
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -5274,11 +6126,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
5274
6126
|
|
|
5275
6127
|
// src/server/routes/dispatch-actions.ts
|
|
5276
6128
|
var import_node_crypto6 = require("crypto");
|
|
5277
|
-
var
|
|
5278
|
-
var DispatchAdHocRequestSchema =
|
|
5279
|
-
title:
|
|
5280
|
-
description:
|
|
5281
|
-
labels:
|
|
6129
|
+
var import_zod8 = require("zod");
|
|
6130
|
+
var DispatchAdHocRequestSchema = import_zod8.z.object({
|
|
6131
|
+
title: import_zod8.z.string().min(1),
|
|
6132
|
+
description: import_zod8.z.string().optional(),
|
|
6133
|
+
labels: import_zod8.z.array(import_zod8.z.string()).optional()
|
|
5282
6134
|
});
|
|
5283
6135
|
function sendJSON2(res, status, body) {
|
|
5284
6136
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -5375,9 +6227,9 @@ function handleAnalysesRoute(req, res, archive) {
|
|
|
5375
6227
|
}
|
|
5376
6228
|
|
|
5377
6229
|
// src/server/routes/maintenance.ts
|
|
5378
|
-
var
|
|
5379
|
-
var TriggerRequestSchema =
|
|
5380
|
-
taskId:
|
|
6230
|
+
var import_zod9 = require("zod");
|
|
6231
|
+
var TriggerRequestSchema = import_zod9.z.object({
|
|
6232
|
+
taskId: import_zod9.z.string().min(1)
|
|
5381
6233
|
});
|
|
5382
6234
|
function sendJSON3(res, status, body) {
|
|
5383
6235
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -5456,9 +6308,9 @@ function handleMaintenanceRoute(req, res, deps) {
|
|
|
5456
6308
|
// src/server/routes/sessions.ts
|
|
5457
6309
|
var fs11 = __toESM(require("fs/promises"));
|
|
5458
6310
|
var path10 = __toESM(require("path"));
|
|
5459
|
-
var
|
|
5460
|
-
var SessionCreateSchema =
|
|
5461
|
-
sessionId:
|
|
6311
|
+
var import_zod10 = require("zod");
|
|
6312
|
+
var SessionCreateSchema = import_zod10.z.object({
|
|
6313
|
+
sessionId: import_zod10.z.string().min(1)
|
|
5462
6314
|
}).passthrough();
|
|
5463
6315
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5464
6316
|
function isSafeId(id) {
|
|
@@ -5545,7 +6397,7 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
5545
6397
|
return;
|
|
5546
6398
|
}
|
|
5547
6399
|
const body = await readBody(req);
|
|
5548
|
-
const updates =
|
|
6400
|
+
const updates = import_zod10.z.record(import_zod10.z.unknown()).parse(JSON.parse(body));
|
|
5549
6401
|
const sessionFilePath = path10.join(sessionsDir, id, "session.json");
|
|
5550
6402
|
const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
|
|
5551
6403
|
await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
@@ -5664,6 +6516,42 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
5664
6516
|
return true;
|
|
5665
6517
|
}
|
|
5666
6518
|
|
|
6519
|
+
// src/server/routes/local-model.ts
|
|
6520
|
+
function sendJSON4(res, status, body) {
|
|
6521
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6522
|
+
res.end(JSON.stringify(body));
|
|
6523
|
+
}
|
|
6524
|
+
function handleLocalModelRoute(req, res, getStatus) {
|
|
6525
|
+
const { method, url } = req;
|
|
6526
|
+
if (url !== "/api/v1/local-model/status") return false;
|
|
6527
|
+
if (method !== "GET") {
|
|
6528
|
+
sendJSON4(res, 405, { error: "Method not allowed" });
|
|
6529
|
+
return true;
|
|
6530
|
+
}
|
|
6531
|
+
if (!getStatus) {
|
|
6532
|
+
sendJSON4(res, 503, { error: "Local backend not configured" });
|
|
6533
|
+
return true;
|
|
6534
|
+
}
|
|
6535
|
+
const status = getStatus();
|
|
6536
|
+
if (!status) {
|
|
6537
|
+
sendJSON4(res, 503, { error: "Local backend not configured" });
|
|
6538
|
+
return true;
|
|
6539
|
+
}
|
|
6540
|
+
sendJSON4(res, 200, status);
|
|
6541
|
+
return true;
|
|
6542
|
+
}
|
|
6543
|
+
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
6544
|
+
const { method, url } = req;
|
|
6545
|
+
if (url !== "/api/v1/local-models/status") return false;
|
|
6546
|
+
if (method !== "GET") {
|
|
6547
|
+
sendJSON4(res, 405, { error: "Method not allowed" });
|
|
6548
|
+
return true;
|
|
6549
|
+
}
|
|
6550
|
+
const statuses = getStatuses ? getStatuses() : [];
|
|
6551
|
+
sendJSON4(res, 200, statuses);
|
|
6552
|
+
return true;
|
|
6553
|
+
}
|
|
6554
|
+
|
|
5667
6555
|
// src/server/static.ts
|
|
5668
6556
|
var fs12 = __toESM(require("fs"));
|
|
5669
6557
|
var path11 = __toESM(require("path"));
|
|
@@ -5817,6 +6705,8 @@ var OrchestratorServer = class {
|
|
|
5817
6705
|
dispatchAdHoc;
|
|
5818
6706
|
sessionsDir;
|
|
5819
6707
|
maintenanceDeps = null;
|
|
6708
|
+
getLocalModelStatus = null;
|
|
6709
|
+
getLocalModelStatuses = null;
|
|
5820
6710
|
recorder = null;
|
|
5821
6711
|
planWatcher = null;
|
|
5822
6712
|
stateChangeListener;
|
|
@@ -5843,6 +6733,8 @@ var OrchestratorServer = class {
|
|
|
5843
6733
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
5844
6734
|
this.sessionsDir = deps?.sessionsDir ?? path13.resolve(".harness", "sessions");
|
|
5845
6735
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
6736
|
+
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
6737
|
+
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
5846
6738
|
}
|
|
5847
6739
|
wireEvents() {
|
|
5848
6740
|
this.stateChangeListener = (snapshot) => {
|
|
@@ -5869,6 +6761,31 @@ var OrchestratorServer = class {
|
|
|
5869
6761
|
broadcastMaintenance(type, data) {
|
|
5870
6762
|
this.broadcaster.broadcast(type, data);
|
|
5871
6763
|
}
|
|
6764
|
+
/**
|
|
6765
|
+
* Broadcast a local-model status change to dashboard clients.
|
|
6766
|
+
*
|
|
6767
|
+
* Phase 3 routes status events through the existing WebSocket broadcaster
|
|
6768
|
+
* on topic `local-model:status` so test fixtures and dashboard consumers
|
|
6769
|
+
* observe payloads immediately. The project broadcasts via WebSocket; the
|
|
6770
|
+
* spec's "SSE topic" wording is approximate. Phase 5 widens the payload
|
|
6771
|
+
* to `NamedLocalModelStatus` (with `backendName` + `endpoint`); the channel
|
|
6772
|
+
* and bind-before-probe ordering are unchanged.
|
|
6773
|
+
*/
|
|
6774
|
+
broadcastLocalModelStatus(status) {
|
|
6775
|
+
this.broadcaster.broadcast("local-model:status", status);
|
|
6776
|
+
}
|
|
6777
|
+
/**
|
|
6778
|
+
* Update the intelligence pipeline reference after construction.
|
|
6779
|
+
*
|
|
6780
|
+
* The orchestrator constructs the pipeline lazily inside `start()` (the
|
|
6781
|
+
* resolver must observe the server before pipeline construction). The
|
|
6782
|
+
* server is built in the orchestrator constructor with `pipeline: null`,
|
|
6783
|
+
* so it must be told the real pipeline once it's been created — otherwise
|
|
6784
|
+
* `/api/analyze` would always see a null pipeline and return 503.
|
|
6785
|
+
*/
|
|
6786
|
+
setPipeline(pipeline) {
|
|
6787
|
+
this.pipeline = pipeline;
|
|
6788
|
+
}
|
|
5872
6789
|
/**
|
|
5873
6790
|
* Set (or update) the maintenance route dependencies after construction.
|
|
5874
6791
|
* Called by the Orchestrator once the scheduler and reporter are ready.
|
|
@@ -5945,6 +6862,12 @@ var OrchestratorServer = class {
|
|
|
5945
6862
|
if (handleDispatchActionsRoute(req, res, this.dispatchAdHoc)) {
|
|
5946
6863
|
return true;
|
|
5947
6864
|
}
|
|
6865
|
+
if (handleLocalModelRoute(req, res, this.getLocalModelStatus)) {
|
|
6866
|
+
return true;
|
|
6867
|
+
}
|
|
6868
|
+
if (handleLocalModelsRoute(req, res, this.getLocalModelStatuses)) {
|
|
6869
|
+
return true;
|
|
6870
|
+
}
|
|
5948
6871
|
if (handleMaintenanceRoute(req, res, this.maintenanceDeps)) {
|
|
5949
6872
|
return true;
|
|
5950
6873
|
}
|
|
@@ -6509,17 +7432,17 @@ var MaintenanceScheduler = class {
|
|
|
6509
7432
|
// src/maintenance/reporter.ts
|
|
6510
7433
|
var fs14 = __toESM(require("fs"));
|
|
6511
7434
|
var path14 = __toESM(require("path"));
|
|
6512
|
-
var
|
|
6513
|
-
var RunResultSchema =
|
|
6514
|
-
taskId:
|
|
6515
|
-
startedAt:
|
|
6516
|
-
completedAt:
|
|
6517
|
-
status:
|
|
6518
|
-
findings:
|
|
6519
|
-
fixed:
|
|
6520
|
-
prUrl:
|
|
6521
|
-
prUpdated:
|
|
6522
|
-
error:
|
|
7435
|
+
var import_zod11 = require("zod");
|
|
7436
|
+
var RunResultSchema = import_zod11.z.object({
|
|
7437
|
+
taskId: import_zod11.z.string(),
|
|
7438
|
+
startedAt: import_zod11.z.string(),
|
|
7439
|
+
completedAt: import_zod11.z.string(),
|
|
7440
|
+
status: import_zod11.z.enum(["success", "failure", "skipped", "no-issues"]),
|
|
7441
|
+
findings: import_zod11.z.number(),
|
|
7442
|
+
fixed: import_zod11.z.number(),
|
|
7443
|
+
prUrl: import_zod11.z.string().nullable(),
|
|
7444
|
+
prUpdated: import_zod11.z.boolean(),
|
|
7445
|
+
error: import_zod11.z.string().optional()
|
|
6523
7446
|
});
|
|
6524
7447
|
var MAX_HISTORY = 500;
|
|
6525
7448
|
var fallbackLogger = {
|
|
@@ -6546,7 +7469,7 @@ var MaintenanceReporter = class {
|
|
|
6546
7469
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
6547
7470
|
const filePath = path14.join(this.persistDir, "history.json");
|
|
6548
7471
|
const data = await fs14.promises.readFile(filePath, "utf-8");
|
|
6549
|
-
const parsed =
|
|
7472
|
+
const parsed = import_zod11.z.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
6550
7473
|
if (parsed.success) {
|
|
6551
7474
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
6552
7475
|
}
|
|
@@ -6829,13 +7752,43 @@ var TaskRunner = class {
|
|
|
6829
7752
|
};
|
|
6830
7753
|
|
|
6831
7754
|
// src/orchestrator.ts
|
|
7755
|
+
function useCaseForBackendParam(issue, backendParam) {
|
|
7756
|
+
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
7757
|
+
const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
7758
|
+
return { kind: "tier", tier };
|
|
7759
|
+
}
|
|
6832
7760
|
var Orchestrator = class extends import_node_events.EventEmitter {
|
|
6833
7761
|
state;
|
|
6834
7762
|
config;
|
|
6835
7763
|
tracker;
|
|
6836
7764
|
workspace;
|
|
6837
7765
|
hooks;
|
|
6838
|
-
|
|
7766
|
+
/**
|
|
7767
|
+
* Spec 2 SC30 / Task 11: per-dispatch backend factory replaces the
|
|
7768
|
+
* Phase 1 `runner` / `localRunner` two-runner split. Each
|
|
7769
|
+
* `dispatchIssue()` call asks the factory for a `RoutingUseCase`-routed
|
|
7770
|
+
* `AgentBackend`, then wraps it in a fresh `AgentRunner`.
|
|
7771
|
+
*
|
|
7772
|
+
* `AgentRunner` is stateless (just `{ backend, options }`), so
|
|
7773
|
+
* per-dispatch construction is safe and avoids the cross-call state
|
|
7774
|
+
* the old two-runner split had to coordinate.
|
|
7775
|
+
*
|
|
7776
|
+
* Null only in the legacy fallback path: when `migrateAgentConfig`
|
|
7777
|
+
* throws (legacy configs missing supplemental fields, e.g.
|
|
7778
|
+
* `agent.backend='anthropic'` with no `agent.model`) AND no
|
|
7779
|
+
* `overrides.backend` is supplied, factory construction is skipped to
|
|
7780
|
+
* preserve the prior behavior of failing at dispatch time rather than
|
|
7781
|
+
* construction time. Eliminating this fallback is autopilot Phase 4+.
|
|
7782
|
+
*/
|
|
7783
|
+
backendFactory;
|
|
7784
|
+
/**
|
|
7785
|
+
* Test-only: when overrides.backend is provided, dispatch uses this
|
|
7786
|
+
* instance directly (bypassing the factory). Mirrors Phase 1
|
|
7787
|
+
* `overrides.backend → this.runner.backend` behavior so existing
|
|
7788
|
+
* MockBackend-injection tests keep working without touching the
|
|
7789
|
+
* factory's routing path.
|
|
7790
|
+
*/
|
|
7791
|
+
overrideBackend;
|
|
6839
7792
|
renderer;
|
|
6840
7793
|
promptTemplate;
|
|
6841
7794
|
server;
|
|
@@ -6843,7 +7796,22 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6843
7796
|
heartbeatInterval;
|
|
6844
7797
|
logger;
|
|
6845
7798
|
interactionQueue;
|
|
6846
|
-
|
|
7799
|
+
/**
|
|
7800
|
+
* Per-named-backend resolver map (Spec 2 SC37). Each `local`/`pi` entry
|
|
7801
|
+
* in `agent.backends` spawns one `LocalModelResolver`. Legacy
|
|
7802
|
+
* single-backend configs converge here via `migrateAgentConfig` (Task 9),
|
|
7803
|
+
* so this map is the single source of truth post-migration.
|
|
7804
|
+
*/
|
|
7805
|
+
localResolvers = /* @__PURE__ */ new Map();
|
|
7806
|
+
/**
|
|
7807
|
+
* Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
|
|
7808
|
+
* (SC39): each local/pi resolver gets its own listener emitting a
|
|
7809
|
+
* `NamedLocalModelStatus` event tagged with `backendName` + `endpoint`.
|
|
7810
|
+
* The previous single-resolver field (`localModelStatusUnsubscribe`)
|
|
7811
|
+
* is replaced by this list so multi-local configs can teardown all
|
|
7812
|
+
* listeners on `stop()` without a Map mutation.
|
|
7813
|
+
*/
|
|
7814
|
+
localModelStatusUnsubscribes = [];
|
|
6847
7815
|
pipeline;
|
|
6848
7816
|
analysisArchive;
|
|
6849
7817
|
graphStore = null;
|
|
@@ -6885,20 +7853,60 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6885
7853
|
this.promptTemplate = promptTemplate;
|
|
6886
7854
|
this.state = createEmptyState(config);
|
|
6887
7855
|
this.logger = new StructuredLogger();
|
|
7856
|
+
try {
|
|
7857
|
+
const migrationResult = migrateAgentConfig(this.config.agent);
|
|
7858
|
+
if (migrationResult.warnings.length > 0) {
|
|
7859
|
+
for (const w of migrationResult.warnings) this.logger.warn(w);
|
|
7860
|
+
}
|
|
7861
|
+
this.config = { ...this.config, agent: migrationResult.config };
|
|
7862
|
+
} catch (err) {
|
|
7863
|
+
this.logger.warn(
|
|
7864
|
+
`migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
7865
|
+
);
|
|
7866
|
+
}
|
|
6888
7867
|
this.tracker = overrides?.tracker || this.createTracker();
|
|
6889
7868
|
this.workspace = new WorkspaceManager(config.workspace);
|
|
6890
7869
|
this.hooks = new WorkspaceHooks(config.hooks);
|
|
6891
7870
|
this.renderer = new PromptRenderer();
|
|
6892
|
-
this.
|
|
6893
|
-
maxTurns: config.agent.maxTurns
|
|
6894
|
-
});
|
|
7871
|
+
this.overrideBackend = overrides?.backend ?? null;
|
|
6895
7872
|
this.interactionQueue = new InteractionQueue(
|
|
6896
7873
|
path15.join(config.workspace.root, "..", "interactions")
|
|
6897
7874
|
);
|
|
6898
7875
|
this.analysisArchive = new AnalysisArchive(path15.join(config.workspace.root, "..", "analyses"));
|
|
6899
|
-
const
|
|
6900
|
-
|
|
6901
|
-
|
|
7876
|
+
const backendsMap = this.config.agent.backends ?? {};
|
|
7877
|
+
for (const [name, def] of Object.entries(backendsMap)) {
|
|
7878
|
+
if (def.type === "local" || def.type === "pi") {
|
|
7879
|
+
const resolverOpts = {
|
|
7880
|
+
endpoint: def.endpoint,
|
|
7881
|
+
configured: typeof def.model === "string" ? [def.model] : def.model,
|
|
7882
|
+
logger: this.logger
|
|
7883
|
+
};
|
|
7884
|
+
if (def.apiKey !== void 0) resolverOpts.apiKey = def.apiKey;
|
|
7885
|
+
if (def.probeIntervalMs !== void 0) resolverOpts.probeIntervalMs = def.probeIntervalMs;
|
|
7886
|
+
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
7890
|
+
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
7891
|
+
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
7892
|
+
const routing = this.config.agent.routing ?? {
|
|
7893
|
+
default: firstBackendName ?? "primary"
|
|
7894
|
+
};
|
|
7895
|
+
this.backendFactory = new OrchestratorBackendFactory({
|
|
7896
|
+
backends: this.config.agent.backends,
|
|
7897
|
+
routing,
|
|
7898
|
+
sandboxPolicy,
|
|
7899
|
+
...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
|
|
7900
|
+
...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
|
|
7901
|
+
getResolverModelFor: (name) => {
|
|
7902
|
+
const resolver = this.localResolvers.get(name);
|
|
7903
|
+
return resolver ? () => resolver.resolveModel() : void 0;
|
|
7904
|
+
}
|
|
7905
|
+
});
|
|
7906
|
+
} else {
|
|
7907
|
+
this.backendFactory = null;
|
|
7908
|
+
}
|
|
7909
|
+
this.pipeline = null;
|
|
6902
7910
|
this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
|
|
6903
7911
|
this.prDetector = new PRDetector({
|
|
6904
7912
|
logger: this.logger,
|
|
@@ -6942,7 +7950,25 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6942
7950
|
pipeline: this.pipeline,
|
|
6943
7951
|
analysisArchive: this.analysisArchive,
|
|
6944
7952
|
roadmapPath: config.tracker.filePath ?? null,
|
|
6945
|
-
dispatchAdHoc: this.dispatchAdHoc.bind(this)
|
|
7953
|
+
dispatchAdHoc: this.dispatchAdHoc.bind(this),
|
|
7954
|
+
getLocalModelStatus: () => {
|
|
7955
|
+
const first = this.localResolvers.values().next();
|
|
7956
|
+
return first.done ? null : first.value.getStatus();
|
|
7957
|
+
},
|
|
7958
|
+
getLocalModelStatuses: () => {
|
|
7959
|
+
const backends = this.config.agent.backends ?? {};
|
|
7960
|
+
const out = [];
|
|
7961
|
+
for (const [name, resolver] of this.localResolvers) {
|
|
7962
|
+
const def = backends[name];
|
|
7963
|
+
if (!def || def.type !== "local" && def.type !== "pi") continue;
|
|
7964
|
+
out.push({
|
|
7965
|
+
...resolver.getStatus(),
|
|
7966
|
+
backendName: name,
|
|
7967
|
+
endpoint: def.endpoint
|
|
7968
|
+
});
|
|
7969
|
+
}
|
|
7970
|
+
return out;
|
|
7971
|
+
}
|
|
6946
7972
|
});
|
|
6947
7973
|
this.server.setRecorder(this.recorder);
|
|
6948
7974
|
this.interactionQueue.onPush((interaction) => {
|
|
@@ -6956,44 +7982,6 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6956
7982
|
}
|
|
6957
7983
|
throw new Error(`Unsupported tracker kind: ${this.config.tracker.kind}`);
|
|
6958
7984
|
}
|
|
6959
|
-
createBackend() {
|
|
6960
|
-
let backend;
|
|
6961
|
-
if (this.config.agent.backend === "mock") {
|
|
6962
|
-
backend = new MockBackend();
|
|
6963
|
-
} else if (this.config.agent.backend === "claude") {
|
|
6964
|
-
backend = new ClaudeBackend(this.config.agent.command);
|
|
6965
|
-
} else if (this.config.agent.backend === "openai") {
|
|
6966
|
-
backend = new OpenAIBackend({
|
|
6967
|
-
...this.config.agent.model !== void 0 && { model: this.config.agent.model },
|
|
6968
|
-
...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
|
|
6969
|
-
});
|
|
6970
|
-
} else if (this.config.agent.backend === "gemini") {
|
|
6971
|
-
backend = new GeminiBackend({
|
|
6972
|
-
...this.config.agent.model !== void 0 && { model: this.config.agent.model },
|
|
6973
|
-
...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
|
|
6974
|
-
});
|
|
6975
|
-
} else if (this.config.agent.backend === "anthropic") {
|
|
6976
|
-
backend = new AnthropicBackend({
|
|
6977
|
-
...this.config.agent.model !== void 0 && { model: this.config.agent.model },
|
|
6978
|
-
...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
|
|
6979
|
-
});
|
|
6980
|
-
} else {
|
|
6981
|
-
throw new Error(`Unsupported agent backend: ${this.config.agent.backend}`);
|
|
6982
|
-
}
|
|
6983
|
-
if (this.config.agent.sandboxPolicy === "docker" && this.config.agent.container) {
|
|
6984
|
-
const runtime = new DockerRuntime();
|
|
6985
|
-
const secretBackend = this.config.agent.secrets ? createSecretBackend(this.config.agent.secrets) : null;
|
|
6986
|
-
const secretKeys = this.config.agent.secrets?.keys ?? [];
|
|
6987
|
-
backend = new ContainerBackend(
|
|
6988
|
-
backend,
|
|
6989
|
-
runtime,
|
|
6990
|
-
secretBackend,
|
|
6991
|
-
this.config.agent.container,
|
|
6992
|
-
secretKeys
|
|
6993
|
-
);
|
|
6994
|
-
}
|
|
6995
|
-
return backend;
|
|
6996
|
-
}
|
|
6997
7985
|
/**
|
|
6998
7986
|
* Creates a TaskRunner for the maintenance scheduler.
|
|
6999
7987
|
* CheckCommandRunner and CommandExecutor use real child_process execution.
|
|
@@ -7119,97 +8107,98 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7119
8107
|
});
|
|
7120
8108
|
}
|
|
7121
8109
|
}
|
|
7122
|
-
createLocalBackend() {
|
|
7123
|
-
if (this.config.agent.localBackend === "openai-compatible") {
|
|
7124
|
-
const localConfig = {};
|
|
7125
|
-
if (this.config.agent.localEndpoint) localConfig.endpoint = this.config.agent.localEndpoint;
|
|
7126
|
-
if (this.config.agent.localModel) localConfig.model = this.config.agent.localModel;
|
|
7127
|
-
if (this.config.agent.localApiKey) localConfig.apiKey = this.config.agent.localApiKey;
|
|
7128
|
-
if (this.config.agent.localTimeoutMs)
|
|
7129
|
-
localConfig.timeoutMs = this.config.agent.localTimeoutMs;
|
|
7130
|
-
return new LocalBackend(localConfig);
|
|
7131
|
-
}
|
|
7132
|
-
if (this.config.agent.localBackend === "pi") {
|
|
7133
|
-
return new PiBackend({
|
|
7134
|
-
model: this.config.agent.localModel,
|
|
7135
|
-
endpoint: this.config.agent.localEndpoint,
|
|
7136
|
-
apiKey: this.config.agent.localApiKey
|
|
7137
|
-
});
|
|
7138
|
-
}
|
|
7139
|
-
return null;
|
|
7140
|
-
}
|
|
7141
8110
|
createIntelligencePipeline() {
|
|
7142
8111
|
const intel = this.config.intelligence;
|
|
7143
8112
|
if (!intel?.enabled) return null;
|
|
7144
|
-
const
|
|
7145
|
-
if (!
|
|
8113
|
+
const selProvider = this.createAnalysisProvider("sel");
|
|
8114
|
+
if (!selProvider) return null;
|
|
8115
|
+
const routing = this.config.agent.routing;
|
|
8116
|
+
const peslName = routing?.intelligence?.pesl;
|
|
8117
|
+
const selName = routing?.intelligence?.sel ?? routing?.default;
|
|
8118
|
+
const peslProvider = peslName !== void 0 && peslName !== selName ? this.createAnalysisProvider("pesl") : null;
|
|
7146
8119
|
const peslModel = intel.models?.pesl ?? this.config.agent.model;
|
|
7147
8120
|
const store = new import_graph.GraphStore();
|
|
7148
8121
|
this.graphStore = store;
|
|
7149
|
-
return new
|
|
7150
|
-
...peslModel !== void 0 && { peslModel }
|
|
8122
|
+
return new import_intelligence4.IntelligencePipeline(selProvider, store, {
|
|
8123
|
+
...peslModel !== void 0 && { peslModel },
|
|
8124
|
+
...peslProvider !== null && peslProvider !== void 0 && { peslProvider }
|
|
7151
8125
|
});
|
|
7152
8126
|
}
|
|
7153
8127
|
/**
|
|
7154
|
-
* Create the AnalysisProvider for
|
|
8128
|
+
* Create the AnalysisProvider for an intelligence-pipeline layer
|
|
8129
|
+
* (`sel` by default; `pesl` when constructing a distinct PESL
|
|
8130
|
+
* provider per Spec 2 SC35).
|
|
8131
|
+
*
|
|
8132
|
+
* Spec 2 Phase 4 (SC31–SC36) — resolution order:
|
|
8133
|
+
* 1. Explicit `intelligence.provider` config wins (preserves Phase 0–3 behavior; SC33).
|
|
8134
|
+
* 2. Otherwise, consult `agent.routing.intelligence.<layer>` (or
|
|
8135
|
+
* `routing.default`) to pick a `BackendDef` from `agent.backends`,
|
|
8136
|
+
* then translate via `buildAnalysisProvider` (the per-type factory).
|
|
7155
8137
|
*
|
|
7156
|
-
*
|
|
7157
|
-
*
|
|
7158
|
-
*
|
|
7159
|
-
*
|
|
8138
|
+
* Closes the Phase 2 deferral (P2-DEF-638): the legacy
|
|
8139
|
+
* `this.config.agent.backend` read at the bottom of this method is
|
|
8140
|
+
* removed; routing is the sole source for non-explicit configs.
|
|
8141
|
+
*
|
|
8142
|
+
* Cyclomatic complexity: pre-Phase-4 was 33 (factory dispatch was
|
|
8143
|
+
* inlined here). Phase 4 extracts the per-type tree into
|
|
8144
|
+
* `buildAnalysisProvider`, dropping this method to ≤ 5 branches
|
|
8145
|
+
* (under the 15 threshold).
|
|
7160
8146
|
*/
|
|
7161
|
-
createAnalysisProvider() {
|
|
8147
|
+
createAnalysisProvider(layer = "sel") {
|
|
7162
8148
|
const intel = this.config.intelligence;
|
|
7163
|
-
|
|
7164
|
-
if (intel
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
const model = selModel ?? this.config.agent.localModel;
|
|
7171
|
-
this.logger.info(`Intelligence pipeline using local backend at ${endpoint}`);
|
|
7172
|
-
return new import_intelligence3.OpenAICompatibleAnalysisProvider({
|
|
7173
|
-
apiKey,
|
|
7174
|
-
baseUrl: endpoint,
|
|
7175
|
-
...model !== void 0 && { defaultModel: model },
|
|
7176
|
-
...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs },
|
|
7177
|
-
...intel?.promptSuffix !== void 0 && { promptSuffix: intel.promptSuffix },
|
|
7178
|
-
...intel?.jsonMode !== void 0 && { jsonMode: intel.jsonMode }
|
|
7179
|
-
});
|
|
7180
|
-
}
|
|
7181
|
-
const backend = this.config.agent.backend;
|
|
7182
|
-
if (backend === "anthropic" || backend === "claude") {
|
|
7183
|
-
const apiKey = this.config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
7184
|
-
if (apiKey) {
|
|
7185
|
-
return new import_intelligence3.AnthropicAnalysisProvider({
|
|
7186
|
-
apiKey,
|
|
7187
|
-
...selModel !== void 0 && { defaultModel: selModel }
|
|
7188
|
-
});
|
|
7189
|
-
}
|
|
7190
|
-
}
|
|
7191
|
-
if (backend === "openai") {
|
|
7192
|
-
const apiKey = this.config.agent.apiKey ?? process.env.OPENAI_API_KEY;
|
|
7193
|
-
if (apiKey) {
|
|
7194
|
-
return new import_intelligence3.OpenAICompatibleAnalysisProvider({
|
|
7195
|
-
apiKey,
|
|
7196
|
-
baseUrl: "https://api.openai.com/v1",
|
|
7197
|
-
...selModel !== void 0 && { defaultModel: selModel }
|
|
7198
|
-
});
|
|
7199
|
-
}
|
|
8149
|
+
if (!intel?.enabled) return null;
|
|
8150
|
+
if (intel.provider) {
|
|
8151
|
+
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
8152
|
+
return this.createProviderFromExplicitConfig(
|
|
8153
|
+
intel.provider,
|
|
8154
|
+
layerModel ?? this.config.agent.model
|
|
8155
|
+
);
|
|
7200
8156
|
}
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
8157
|
+
const routed = this.resolveRoutedBackendForIntelligence(layer);
|
|
8158
|
+
if (!routed) return null;
|
|
8159
|
+
const { name, def } = routed;
|
|
8160
|
+
const resolver = this.localResolvers.get(name);
|
|
8161
|
+
return buildAnalysisProvider({
|
|
8162
|
+
def,
|
|
8163
|
+
backendName: name,
|
|
8164
|
+
layer,
|
|
8165
|
+
// Spec 2 P3-IMP-1: a single snapshot read feeds the factory's
|
|
8166
|
+
// unavailable-warn diagnostic (Configured/Detected lists) and
|
|
8167
|
+
// collapses the two `getStatus()` calls flagged by P3-SUG-2.
|
|
8168
|
+
getResolverStatusSnapshot: () => {
|
|
8169
|
+
if (!resolver) return null;
|
|
8170
|
+
const status = resolver.getStatus();
|
|
8171
|
+
return {
|
|
8172
|
+
available: status.available,
|
|
8173
|
+
resolved: status.resolved,
|
|
8174
|
+
configured: status.configured,
|
|
8175
|
+
detected: status.detected
|
|
8176
|
+
};
|
|
8177
|
+
},
|
|
8178
|
+
intelligence: intel,
|
|
8179
|
+
logger: this.logger
|
|
8180
|
+
});
|
|
8181
|
+
}
|
|
8182
|
+
/**
|
|
8183
|
+
* Look up the routed BackendDef for an intelligence layer, falling
|
|
8184
|
+
* back through `routing.intelligence.<layer>` → `routing.default`
|
|
8185
|
+
* → null. Returns the resolved name alongside the def so callers can
|
|
8186
|
+
* key into the per-name resolver map.
|
|
8187
|
+
*/
|
|
8188
|
+
resolveRoutedBackendForIntelligence(layer) {
|
|
8189
|
+
const routing = this.config.agent.routing;
|
|
8190
|
+
const backends = this.config.agent.backends;
|
|
8191
|
+
if (!routing || !backends) return null;
|
|
8192
|
+
const layerName = routing.intelligence?.[layer];
|
|
8193
|
+
const name = layerName ?? routing.default;
|
|
8194
|
+
const def = backends[name];
|
|
8195
|
+
if (!def) {
|
|
8196
|
+
this.logger.warn(
|
|
8197
|
+
`Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
|
|
8198
|
+
);
|
|
8199
|
+
return null;
|
|
7208
8200
|
}
|
|
7209
|
-
|
|
7210
|
-
`Intelligence pipeline: unsupported backend "${backend}". Supported: anthropic, claude, openai, or localBackend: openai-compatible / pi.`
|
|
7211
|
-
);
|
|
7212
|
-
return null;
|
|
8201
|
+
return { name, def };
|
|
7213
8202
|
}
|
|
7214
8203
|
createProviderFromExplicitConfig(provider, selModel) {
|
|
7215
8204
|
if (provider.kind === "anthropic") {
|
|
@@ -7217,13 +8206,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7217
8206
|
if (!apiKey2) {
|
|
7218
8207
|
throw new Error("Intelligence pipeline: no Anthropic API key found.");
|
|
7219
8208
|
}
|
|
7220
|
-
return new
|
|
8209
|
+
return new import_intelligence4.AnthropicAnalysisProvider({
|
|
7221
8210
|
apiKey: apiKey2,
|
|
7222
8211
|
...selModel !== void 0 && { defaultModel: selModel }
|
|
7223
8212
|
});
|
|
7224
8213
|
}
|
|
7225
8214
|
if (provider.kind === "claude-cli") {
|
|
7226
|
-
return new
|
|
8215
|
+
return new import_intelligence4.ClaudeCliAnalysisProvider({
|
|
7227
8216
|
command: this.config.agent.command,
|
|
7228
8217
|
...selModel !== void 0 && { defaultModel: selModel },
|
|
7229
8218
|
...this.config.intelligence?.requestTimeoutMs !== void 0 && {
|
|
@@ -7234,7 +8223,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7234
8223
|
const apiKey = provider.apiKey ?? this.config.agent.apiKey ?? "ollama";
|
|
7235
8224
|
const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
|
|
7236
8225
|
const intel = this.config.intelligence;
|
|
7237
|
-
return new
|
|
8226
|
+
return new import_intelligence4.OpenAICompatibleAnalysisProvider({
|
|
7238
8227
|
apiKey,
|
|
7239
8228
|
baseUrl,
|
|
7240
8229
|
...selModel !== void 0 && { defaultModel: selModel },
|
|
@@ -7716,9 +8705,18 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7716
8705
|
issue,
|
|
7717
8706
|
attempt: attempt || 1
|
|
7718
8707
|
});
|
|
8708
|
+
const useCase = useCaseForBackendParam(issue, backend);
|
|
8709
|
+
let routedBackendName;
|
|
8710
|
+
if (this.overrideBackend !== null) {
|
|
8711
|
+
routedBackendName = this.overrideBackend.name;
|
|
8712
|
+
} else if (this.backendFactory !== null) {
|
|
8713
|
+
routedBackendName = this.backendFactory.resolveName(useCase);
|
|
8714
|
+
} else {
|
|
8715
|
+
routedBackendName = this.config.agent.routing?.default ?? this.config.agent.backend ?? "unknown";
|
|
8716
|
+
}
|
|
7719
8717
|
const session = {
|
|
7720
8718
|
sessionId: `pending-${Date.now()}`,
|
|
7721
|
-
backendName:
|
|
8719
|
+
backendName: routedBackendName,
|
|
7722
8720
|
agentPid: null,
|
|
7723
8721
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7724
8722
|
lastEvent: "Dispatching",
|
|
@@ -7745,11 +8743,23 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7745
8743
|
issue.id,
|
|
7746
8744
|
issue.externalId ?? null,
|
|
7747
8745
|
issue.identifier,
|
|
7748
|
-
|
|
8746
|
+
routedBackendName,
|
|
7749
8747
|
attempt ?? 1,
|
|
7750
8748
|
issue.title
|
|
7751
8749
|
);
|
|
7752
|
-
|
|
8750
|
+
let agentBackend;
|
|
8751
|
+
if (this.overrideBackend !== null) {
|
|
8752
|
+
agentBackend = this.overrideBackend;
|
|
8753
|
+
} else if (this.backendFactory !== null) {
|
|
8754
|
+
agentBackend = this.backendFactory.forUseCase(useCase);
|
|
8755
|
+
} else {
|
|
8756
|
+
throw new Error(
|
|
8757
|
+
`Cannot dispatch ${issue.identifier}: agent.backends not synthesized (migration failed) and no override backend supplied. Migrate to agent.backends/agent.routing per docs/guides/multi-backend-routing.md.`
|
|
8758
|
+
);
|
|
8759
|
+
}
|
|
8760
|
+
const activeRunner = new AgentRunner(agentBackend, {
|
|
8761
|
+
maxTurns: this.config.agent.maxTurns
|
|
8762
|
+
});
|
|
7753
8763
|
this.runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, activeRunner);
|
|
7754
8764
|
} catch (error) {
|
|
7755
8765
|
this.logger.error(`Dispatch failed for ${issue.identifier}`, { error: String(error) });
|
|
@@ -7782,7 +8792,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7782
8792
|
}
|
|
7783
8793
|
}
|
|
7784
8794
|
runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, runner) {
|
|
7785
|
-
const activeRunner = runner
|
|
8795
|
+
const activeRunner = runner;
|
|
7786
8796
|
this.logger.info(`Starting background task for ${issue.identifier}`);
|
|
7787
8797
|
const abortController = new AbortController();
|
|
7788
8798
|
this.abortControllers.set(issue.id, { controller: abortController, pid: null });
|
|
@@ -7903,6 +8913,42 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7903
8913
|
this.emit("state_change", this.getSnapshot());
|
|
7904
8914
|
await this.dispatchIssue(issue, 1, "local");
|
|
7905
8915
|
}
|
|
8916
|
+
/**
|
|
8917
|
+
* Initialize the LocalModelResolver and intelligence pipeline.
|
|
8918
|
+
*
|
|
8919
|
+
* Runs the initial probe (so resolver state reflects server availability)
|
|
8920
|
+
* before constructing the intelligence pipeline. Subscribes the dashboard
|
|
8921
|
+
* broadcast stub to status changes. Called exactly once from start().
|
|
8922
|
+
*/
|
|
8923
|
+
async initLocalModelAndPipeline() {
|
|
8924
|
+
if (this.localResolvers.size > 0) {
|
|
8925
|
+
const backends = this.config.agent.backends ?? {};
|
|
8926
|
+
for (const [name, resolver] of this.localResolvers) {
|
|
8927
|
+
const def = backends[name];
|
|
8928
|
+
if (!def || def.type !== "local" && def.type !== "pi") {
|
|
8929
|
+
this.logger.warn("Resolver without matching backend def \u2014 broadcast skipped", {
|
|
8930
|
+
name
|
|
8931
|
+
});
|
|
8932
|
+
continue;
|
|
8933
|
+
}
|
|
8934
|
+
const endpoint = def.endpoint;
|
|
8935
|
+
const unsubscribe = resolver.onStatusChange((status) => {
|
|
8936
|
+
const named = {
|
|
8937
|
+
...status,
|
|
8938
|
+
backendName: name,
|
|
8939
|
+
endpoint
|
|
8940
|
+
};
|
|
8941
|
+
this.server?.broadcastLocalModelStatus(named);
|
|
8942
|
+
});
|
|
8943
|
+
this.localModelStatusUnsubscribes.push(unsubscribe);
|
|
8944
|
+
}
|
|
8945
|
+
for (const resolver of this.localResolvers.values()) {
|
|
8946
|
+
await resolver.start();
|
|
8947
|
+
}
|
|
8948
|
+
}
|
|
8949
|
+
this.pipeline = this.createIntelligencePipeline();
|
|
8950
|
+
this.server?.setPipeline(this.pipeline);
|
|
8951
|
+
}
|
|
7906
8952
|
/**
|
|
7907
8953
|
* Starts the polling loop and the internal HTTP server.
|
|
7908
8954
|
* Runs startup reconciliation to release orphaned claims before the first tick.
|
|
@@ -7911,6 +8957,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7911
8957
|
if (this.server) {
|
|
7912
8958
|
void this.server.start();
|
|
7913
8959
|
}
|
|
8960
|
+
await this.initLocalModelAndPipeline();
|
|
7914
8961
|
await this.ensureClaimManager();
|
|
7915
8962
|
const runningIssueIds = new Set(this.state.running.keys());
|
|
7916
8963
|
const reconcileResult = await this.claimManager.reconcileOnStartup(runningIssueIds);
|
|
@@ -7962,6 +9009,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7962
9009
|
clearInterval(this.heartbeatInterval);
|
|
7963
9010
|
this.heartbeatInterval = void 0;
|
|
7964
9011
|
}
|
|
9012
|
+
for (const unsub of this.localModelStatusUnsubscribes) {
|
|
9013
|
+
unsub();
|
|
9014
|
+
}
|
|
9015
|
+
this.localModelStatusUnsubscribes = [];
|
|
9016
|
+
for (const resolver of this.localResolvers.values()) {
|
|
9017
|
+
resolver.stop();
|
|
9018
|
+
}
|
|
7965
9019
|
if (this.maintenanceScheduler) {
|
|
7966
9020
|
this.maintenanceScheduler.stop();
|
|
7967
9021
|
this.maintenanceScheduler = null;
|
|
@@ -8239,12 +9293,14 @@ function launchTUI(orchestrator) {
|
|
|
8239
9293
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8240
9294
|
0 && (module.exports = {
|
|
8241
9295
|
AnalysisArchive,
|
|
9296
|
+
BackendRouter,
|
|
8242
9297
|
ClaimManager,
|
|
8243
9298
|
InteractionQueue,
|
|
8244
9299
|
LinearGraphQLStub,
|
|
8245
9300
|
MockBackend,
|
|
8246
9301
|
ORCHESTRATOR_IDENTITY_FILE,
|
|
8247
9302
|
Orchestrator,
|
|
9303
|
+
OrchestratorBackendFactory,
|
|
8248
9304
|
PRDetector,
|
|
8249
9305
|
PromptRenderer,
|
|
8250
9306
|
RoadmapTrackerAdapter,
|
|
@@ -8257,6 +9313,7 @@ function launchTUI(orchestrator) {
|
|
|
8257
9313
|
calculateRetryDelay,
|
|
8258
9314
|
canDispatch,
|
|
8259
9315
|
computeRateLimitDelay,
|
|
9316
|
+
createBackend,
|
|
8260
9317
|
createEmptyState,
|
|
8261
9318
|
detectScopeTier,
|
|
8262
9319
|
extractHighlights,
|
|
@@ -8267,6 +9324,7 @@ function launchTUI(orchestrator) {
|
|
|
8267
9324
|
isEligible,
|
|
8268
9325
|
launchTUI,
|
|
8269
9326
|
loadPublishedIndex,
|
|
9327
|
+
migrateAgentConfig,
|
|
8270
9328
|
reconcile,
|
|
8271
9329
|
renderAnalysisComment,
|
|
8272
9330
|
renderPRComment,
|