@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.mjs
CHANGED
|
@@ -1771,8 +1771,92 @@ import { parse } from "yaml";
|
|
|
1771
1771
|
import { Ok as Ok3, Err as Err2 } from "@harness-engineering/types";
|
|
1772
1772
|
|
|
1773
1773
|
// src/workflow/config.ts
|
|
1774
|
-
import {
|
|
1774
|
+
import { z as z2 } from "zod";
|
|
1775
|
+
import {
|
|
1776
|
+
Ok as Ok2,
|
|
1777
|
+
Err
|
|
1778
|
+
} from "@harness-engineering/types";
|
|
1779
|
+
|
|
1780
|
+
// src/workflow/schema.ts
|
|
1781
|
+
import { z } from "zod";
|
|
1782
|
+
var ModelSchema = z.union([z.string().min(1), z.array(z.string().min(1)).nonempty()], {
|
|
1783
|
+
errorMap: () => ({
|
|
1784
|
+
message: "model must be a non-empty string or array of strings"
|
|
1785
|
+
})
|
|
1786
|
+
});
|
|
1787
|
+
var BackendDefSchema = z.discriminatedUnion("type", [
|
|
1788
|
+
z.object({ type: z.literal("mock") }).strict(),
|
|
1789
|
+
z.object({
|
|
1790
|
+
type: z.literal("claude"),
|
|
1791
|
+
command: z.string().optional()
|
|
1792
|
+
}).strict(),
|
|
1793
|
+
z.object({
|
|
1794
|
+
type: z.literal("anthropic"),
|
|
1795
|
+
model: z.string().min(1),
|
|
1796
|
+
apiKey: z.string().optional()
|
|
1797
|
+
}).strict(),
|
|
1798
|
+
z.object({
|
|
1799
|
+
type: z.literal("openai"),
|
|
1800
|
+
model: z.string().min(1),
|
|
1801
|
+
apiKey: z.string().optional()
|
|
1802
|
+
}).strict(),
|
|
1803
|
+
z.object({
|
|
1804
|
+
type: z.literal("gemini"),
|
|
1805
|
+
model: z.string().min(1),
|
|
1806
|
+
apiKey: z.string().optional()
|
|
1807
|
+
}).strict(),
|
|
1808
|
+
z.object({
|
|
1809
|
+
type: z.literal("local"),
|
|
1810
|
+
endpoint: z.string().url(),
|
|
1811
|
+
model: ModelSchema,
|
|
1812
|
+
apiKey: z.string().optional(),
|
|
1813
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
1814
|
+
probeIntervalMs: z.number().int().min(1e3).optional()
|
|
1815
|
+
}).strict(),
|
|
1816
|
+
z.object({
|
|
1817
|
+
type: z.literal("pi"),
|
|
1818
|
+
endpoint: z.string().url(),
|
|
1819
|
+
model: ModelSchema,
|
|
1820
|
+
apiKey: z.string().optional(),
|
|
1821
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
1822
|
+
probeIntervalMs: z.number().int().min(1e3).optional()
|
|
1823
|
+
}).strict()
|
|
1824
|
+
]);
|
|
1825
|
+
var RoutingConfigSchema = z.object({
|
|
1826
|
+
default: z.string().min(1),
|
|
1827
|
+
"quick-fix": z.string().optional(),
|
|
1828
|
+
"guided-change": z.string().optional(),
|
|
1829
|
+
"full-exploration": z.string().optional(),
|
|
1830
|
+
diagnostic: z.string().optional(),
|
|
1831
|
+
intelligence: z.object({
|
|
1832
|
+
sel: z.string().optional(),
|
|
1833
|
+
pesl: z.string().optional()
|
|
1834
|
+
}).strict().optional()
|
|
1835
|
+
}).strict();
|
|
1836
|
+
|
|
1837
|
+
// src/workflow/config.ts
|
|
1775
1838
|
var REQUIRED_SECTIONS = ["tracker", "polling", "workspace", "hooks", "agent", "server"];
|
|
1839
|
+
var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
|
|
1840
|
+
function crossFieldRoutingIssues(backends, routing) {
|
|
1841
|
+
const issues = [];
|
|
1842
|
+
const names = new Set(Object.keys(backends));
|
|
1843
|
+
const checkRef = (path16, name) => {
|
|
1844
|
+
if (name !== void 0 && !names.has(name)) {
|
|
1845
|
+
issues.push({
|
|
1846
|
+
path: path16,
|
|
1847
|
+
message: `routing.${path16.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
checkRef(["default"], routing.default);
|
|
1852
|
+
checkRef(["quick-fix"], routing["quick-fix"]);
|
|
1853
|
+
checkRef(["guided-change"], routing["guided-change"]);
|
|
1854
|
+
checkRef(["full-exploration"], routing["full-exploration"]);
|
|
1855
|
+
checkRef(["diagnostic"], routing.diagnostic);
|
|
1856
|
+
checkRef(["intelligence", "sel"], routing.intelligence?.sel);
|
|
1857
|
+
checkRef(["intelligence", "pesl"], routing.intelligence?.pesl);
|
|
1858
|
+
return issues;
|
|
1859
|
+
}
|
|
1776
1860
|
function validateWorkflowConfig(config) {
|
|
1777
1861
|
if (!config || typeof config !== "object")
|
|
1778
1862
|
return Err(new Error("Config is missing or not an object"));
|
|
@@ -1783,6 +1867,35 @@ function validateWorkflowConfig(config) {
|
|
|
1783
1867
|
if (c.intelligence !== void 0 && (typeof c.intelligence !== "object" || c.intelligence === null)) {
|
|
1784
1868
|
return Err(new Error("Config intelligence section must be an object if present"));
|
|
1785
1869
|
}
|
|
1870
|
+
const agent = c.agent ?? {};
|
|
1871
|
+
const hasLegacyBackend = typeof agent.backend === "string" && agent.backend.length > 0;
|
|
1872
|
+
const hasModernBackends = agent.backends !== void 0 && typeof agent.backends === "object" && agent.backends !== null;
|
|
1873
|
+
if (!hasLegacyBackend && !hasModernBackends) {
|
|
1874
|
+
return Err(new Error("Config must define agent.backend or agent.backends."));
|
|
1875
|
+
}
|
|
1876
|
+
if (hasModernBackends) {
|
|
1877
|
+
const backendsParsed = BackendsMapSchema.safeParse(agent.backends);
|
|
1878
|
+
if (!backendsParsed.success) {
|
|
1879
|
+
return Err(new Error(`agent.backends: ${backendsParsed.error.message}`));
|
|
1880
|
+
}
|
|
1881
|
+
const routingParsed = RoutingConfigSchema.optional().safeParse(agent.routing);
|
|
1882
|
+
if (!routingParsed.success) {
|
|
1883
|
+
return Err(new Error(`agent.routing: ${routingParsed.error.message}`));
|
|
1884
|
+
}
|
|
1885
|
+
if (routingParsed.data) {
|
|
1886
|
+
const cross = crossFieldRoutingIssues(
|
|
1887
|
+
backendsParsed.data,
|
|
1888
|
+
routingParsed.data
|
|
1889
|
+
);
|
|
1890
|
+
if (cross.length > 0) {
|
|
1891
|
+
return Err(
|
|
1892
|
+
new Error(
|
|
1893
|
+
`Cross-field: ${cross.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
1894
|
+
)
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1786
1899
|
return Ok2(config);
|
|
1787
1900
|
}
|
|
1788
1901
|
function getDefaultConfig() {
|
|
@@ -2493,9 +2606,9 @@ import { randomUUID as randomUUID5 } from "crypto";
|
|
|
2493
2606
|
import { writeTaint } from "@harness-engineering/core";
|
|
2494
2607
|
import {
|
|
2495
2608
|
IntelligencePipeline,
|
|
2496
|
-
AnthropicAnalysisProvider,
|
|
2497
|
-
OpenAICompatibleAnalysisProvider,
|
|
2498
|
-
ClaudeCliAnalysisProvider
|
|
2609
|
+
AnthropicAnalysisProvider as AnthropicAnalysisProvider2,
|
|
2610
|
+
OpenAICompatibleAnalysisProvider as OpenAICompatibleAnalysisProvider2,
|
|
2611
|
+
ClaudeCliAnalysisProvider as ClaudeCliAnalysisProvider2
|
|
2499
2612
|
} from "@harness-engineering/intelligence";
|
|
2500
2613
|
import { GraphStore } from "@harness-engineering/graph";
|
|
2501
2614
|
|
|
@@ -3180,6 +3293,428 @@ var AgentRunner = class {
|
|
|
3180
3293
|
}
|
|
3181
3294
|
};
|
|
3182
3295
|
|
|
3296
|
+
// src/agent/local-model-resolver.ts
|
|
3297
|
+
var DEFAULT_PROBE_INTERVAL_MS = 3e4;
|
|
3298
|
+
var MIN_PROBE_INTERVAL_MS = 1e3;
|
|
3299
|
+
var DEFAULT_API_KEY = "lm-studio";
|
|
3300
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
|
|
3301
|
+
var noopLogger = {
|
|
3302
|
+
info: () => void 0,
|
|
3303
|
+
warn: () => void 0
|
|
3304
|
+
};
|
|
3305
|
+
async function defaultFetchModels(endpoint, apiKey, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
3306
|
+
const url = `${endpoint.replace(/\/$/, "")}/models`;
|
|
3307
|
+
let res;
|
|
3308
|
+
try {
|
|
3309
|
+
res = await fetch(url, {
|
|
3310
|
+
headers: { Authorization: `Bearer ${apiKey ?? DEFAULT_API_KEY}` },
|
|
3311
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3312
|
+
});
|
|
3313
|
+
} catch (err) {
|
|
3314
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
3315
|
+
throw new Error(`request timeout (${timeoutMs}ms)`, { cause: err });
|
|
3316
|
+
}
|
|
3317
|
+
throw err;
|
|
3318
|
+
}
|
|
3319
|
+
if (!res.ok) {
|
|
3320
|
+
throw new Error(`probe failed: ${res.status} ${res.statusText}`);
|
|
3321
|
+
}
|
|
3322
|
+
let body;
|
|
3323
|
+
try {
|
|
3324
|
+
body = await res.json();
|
|
3325
|
+
} catch {
|
|
3326
|
+
throw new Error("malformed /v1/models response");
|
|
3327
|
+
}
|
|
3328
|
+
if (!body || typeof body !== "object" || !Array.isArray(body.data)) {
|
|
3329
|
+
throw new Error("malformed /v1/models response");
|
|
3330
|
+
}
|
|
3331
|
+
const data = body.data;
|
|
3332
|
+
const ids = [];
|
|
3333
|
+
for (const entry of data) {
|
|
3334
|
+
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
|
|
3335
|
+
throw new Error("malformed /v1/models response");
|
|
3336
|
+
}
|
|
3337
|
+
ids.push(entry.id);
|
|
3338
|
+
}
|
|
3339
|
+
return ids;
|
|
3340
|
+
}
|
|
3341
|
+
var LocalModelResolver = class {
|
|
3342
|
+
endpoint;
|
|
3343
|
+
apiKey;
|
|
3344
|
+
configured;
|
|
3345
|
+
probeIntervalMs;
|
|
3346
|
+
fetchModels;
|
|
3347
|
+
logger;
|
|
3348
|
+
timer = null;
|
|
3349
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3350
|
+
/**
|
|
3351
|
+
* Tracks an in-flight probe so concurrent invocations (interval tick while a
|
|
3352
|
+
* slow probe is running, or a manual `probe()` call mid-flight) share the
|
|
3353
|
+
* existing promise instead of racing to mutate `detected/resolved/lastError/
|
|
3354
|
+
* warnings` non-atomically across `await` points. Applies to both the timer
|
|
3355
|
+
* callback and direct `probe()` calls — any caller that arrives during an
|
|
3356
|
+
* in-flight probe gets the same promise back. Cleared in `finally` so the
|
|
3357
|
+
* next tick can start a fresh probe.
|
|
3358
|
+
*/
|
|
3359
|
+
probeInFlight = null;
|
|
3360
|
+
// Mutable status fields (composed into LocalModelStatus on demand).
|
|
3361
|
+
resolved = null;
|
|
3362
|
+
detected = [];
|
|
3363
|
+
lastProbeAt = null;
|
|
3364
|
+
lastError = null;
|
|
3365
|
+
warnings = [];
|
|
3366
|
+
available = false;
|
|
3367
|
+
constructor(opts) {
|
|
3368
|
+
this.endpoint = opts.endpoint;
|
|
3369
|
+
if (opts.apiKey !== void 0) {
|
|
3370
|
+
this.apiKey = opts.apiKey;
|
|
3371
|
+
}
|
|
3372
|
+
this.configured = [...opts.configured];
|
|
3373
|
+
const interval = opts.probeIntervalMs ?? DEFAULT_PROBE_INTERVAL_MS;
|
|
3374
|
+
this.probeIntervalMs = Math.max(MIN_PROBE_INTERVAL_MS, interval);
|
|
3375
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3376
|
+
this.fetchModels = opts.fetchModels ?? ((endpoint, apiKey) => defaultFetchModels(endpoint, apiKey, timeoutMs));
|
|
3377
|
+
this.logger = opts.logger ?? noopLogger;
|
|
3378
|
+
}
|
|
3379
|
+
resolveModel() {
|
|
3380
|
+
return this.resolved;
|
|
3381
|
+
}
|
|
3382
|
+
getStatus() {
|
|
3383
|
+
return {
|
|
3384
|
+
available: this.available,
|
|
3385
|
+
resolved: this.resolved,
|
|
3386
|
+
configured: [...this.configured],
|
|
3387
|
+
detected: [...this.detected],
|
|
3388
|
+
lastProbeAt: this.lastProbeAt,
|
|
3389
|
+
lastError: this.lastError,
|
|
3390
|
+
warnings: [...this.warnings]
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
onStatusChange(handler) {
|
|
3394
|
+
this.listeners.add(handler);
|
|
3395
|
+
return () => {
|
|
3396
|
+
this.listeners.delete(handler);
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
async probe() {
|
|
3400
|
+
if (this.probeInFlight !== null) {
|
|
3401
|
+
return this.probeInFlight;
|
|
3402
|
+
}
|
|
3403
|
+
const inFlight = this.runProbe().finally(() => {
|
|
3404
|
+
this.probeInFlight = null;
|
|
3405
|
+
});
|
|
3406
|
+
this.probeInFlight = inFlight;
|
|
3407
|
+
return inFlight;
|
|
3408
|
+
}
|
|
3409
|
+
async runProbe() {
|
|
3410
|
+
const before = this.snapshotForDiff();
|
|
3411
|
+
try {
|
|
3412
|
+
const detected = await this.fetchModels(this.endpoint, this.apiKey);
|
|
3413
|
+
this.detected = [...detected];
|
|
3414
|
+
this.lastError = null;
|
|
3415
|
+
this.lastProbeAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3416
|
+
const match = this.configured.find((id) => detected.includes(id)) ?? null;
|
|
3417
|
+
this.resolved = match;
|
|
3418
|
+
this.available = match !== null;
|
|
3419
|
+
this.warnings = match ? [] : [
|
|
3420
|
+
`No configured local model is loaded. Configured: [${this.configured.join(", ")}]. Detected: [${detected.join(", ")}].`
|
|
3421
|
+
];
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
const message = err instanceof Error ? err.message : "probe failed";
|
|
3424
|
+
this.lastError = message;
|
|
3425
|
+
this.available = false;
|
|
3426
|
+
this.resolved = null;
|
|
3427
|
+
this.warnings = [`Local model probe failed against ${this.endpoint}: ${message}.`];
|
|
3428
|
+
this.logger.warn("local-model-resolver probe failed", {
|
|
3429
|
+
endpoint: this.endpoint,
|
|
3430
|
+
error: message
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
const after = this.snapshotForDiff();
|
|
3434
|
+
const status = this.getStatus();
|
|
3435
|
+
if (before !== after) {
|
|
3436
|
+
for (const listener of this.listeners) {
|
|
3437
|
+
try {
|
|
3438
|
+
listener(status);
|
|
3439
|
+
} catch (err) {
|
|
3440
|
+
this.logger.warn("local-model-resolver listener threw", {
|
|
3441
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
return status;
|
|
3447
|
+
}
|
|
3448
|
+
async start() {
|
|
3449
|
+
if (this.timer !== null) {
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
await this.probe();
|
|
3453
|
+
this.timer = setInterval(() => {
|
|
3454
|
+
void this.probe();
|
|
3455
|
+
}, this.probeIntervalMs);
|
|
3456
|
+
const handle = this.timer;
|
|
3457
|
+
handle.unref?.();
|
|
3458
|
+
}
|
|
3459
|
+
stop() {
|
|
3460
|
+
if (this.timer !== null) {
|
|
3461
|
+
clearInterval(this.timer);
|
|
3462
|
+
this.timer = null;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
snapshotForDiff() {
|
|
3466
|
+
return JSON.stringify({
|
|
3467
|
+
available: this.available,
|
|
3468
|
+
resolved: this.resolved,
|
|
3469
|
+
configured: this.configured,
|
|
3470
|
+
detected: this.detected,
|
|
3471
|
+
lastError: this.lastError,
|
|
3472
|
+
warnings: this.warnings
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
3476
|
+
|
|
3477
|
+
// src/agent/config-migration.ts
|
|
3478
|
+
var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
|
|
3479
|
+
function migrateAgentConfig(agent) {
|
|
3480
|
+
const warnings = [];
|
|
3481
|
+
const legacyFields = [
|
|
3482
|
+
{ path: "agent.backend", present: agent.backend !== void 0 && agent.backend !== "" },
|
|
3483
|
+
{ path: "agent.command", present: agent.command !== void 0 },
|
|
3484
|
+
{ path: "agent.model", present: agent.model !== void 0 },
|
|
3485
|
+
{ path: "agent.apiKey", present: agent.apiKey !== void 0 },
|
|
3486
|
+
{ path: "agent.localBackend", present: agent.localBackend !== void 0 },
|
|
3487
|
+
{ path: "agent.localEndpoint", present: agent.localEndpoint !== void 0 },
|
|
3488
|
+
{ path: "agent.localModel", present: agent.localModel !== void 0 },
|
|
3489
|
+
{ path: "agent.localApiKey", present: agent.localApiKey !== void 0 },
|
|
3490
|
+
{ path: "agent.localTimeoutMs", present: agent.localTimeoutMs !== void 0 },
|
|
3491
|
+
{ path: "agent.localProbeIntervalMs", present: agent.localProbeIntervalMs !== void 0 }
|
|
3492
|
+
];
|
|
3493
|
+
const presentLegacy = legacyFields.filter((f) => f.present).map((f) => f.path);
|
|
3494
|
+
const CASE1_ALWAYS_SUPPRESS = /* @__PURE__ */ new Set(["agent.backend"]);
|
|
3495
|
+
const CASE1_LOCAL_GROUP = /* @__PURE__ */ new Set([
|
|
3496
|
+
"agent.localBackend",
|
|
3497
|
+
"agent.localEndpoint",
|
|
3498
|
+
"agent.localModel",
|
|
3499
|
+
"agent.localApiKey",
|
|
3500
|
+
"agent.localTimeoutMs",
|
|
3501
|
+
"agent.localProbeIntervalMs"
|
|
3502
|
+
]);
|
|
3503
|
+
const suppressLocalGroup = agent.localBackend !== void 0;
|
|
3504
|
+
if (agent.backends !== void 0) {
|
|
3505
|
+
for (const path16 of presentLegacy) {
|
|
3506
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path16)) continue;
|
|
3507
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path16)) continue;
|
|
3508
|
+
warnings.push(
|
|
3509
|
+
`Ignoring legacy field '${path16}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3510
|
+
);
|
|
3511
|
+
}
|
|
3512
|
+
return { config: agent, warnings };
|
|
3513
|
+
}
|
|
3514
|
+
if (presentLegacy.length === 0) {
|
|
3515
|
+
return { config: agent, warnings };
|
|
3516
|
+
}
|
|
3517
|
+
const backends = {};
|
|
3518
|
+
const routing = { default: "primary" };
|
|
3519
|
+
backends.primary = synthesizePrimary(agent);
|
|
3520
|
+
if (agent.localBackend !== void 0) {
|
|
3521
|
+
backends.local = synthesizeLocal(agent);
|
|
3522
|
+
}
|
|
3523
|
+
const autoExec = agent.escalation?.autoExecute ?? [];
|
|
3524
|
+
if (backends.local !== void 0) {
|
|
3525
|
+
for (const tier of autoExec) {
|
|
3526
|
+
routing[tier] = "local";
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
for (const path16 of presentLegacy) {
|
|
3530
|
+
warnings.push(
|
|
3531
|
+
`Deprecated config field '${path16}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
return {
|
|
3535
|
+
config: {
|
|
3536
|
+
...agent,
|
|
3537
|
+
backends,
|
|
3538
|
+
routing
|
|
3539
|
+
},
|
|
3540
|
+
warnings
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
function synthesizePrimary(agent) {
|
|
3544
|
+
const backend = agent.backend;
|
|
3545
|
+
switch (backend) {
|
|
3546
|
+
case "mock":
|
|
3547
|
+
return { type: "mock" };
|
|
3548
|
+
case "claude": {
|
|
3549
|
+
const def = { type: "claude" };
|
|
3550
|
+
if (agent.command !== void 0) def.command = agent.command;
|
|
3551
|
+
return def;
|
|
3552
|
+
}
|
|
3553
|
+
case "anthropic": {
|
|
3554
|
+
if (agent.model === void 0) {
|
|
3555
|
+
throw new Error("migrateAgentConfig: agent.backend='anthropic' requires agent.model");
|
|
3556
|
+
}
|
|
3557
|
+
const def = { type: "anthropic", model: agent.model };
|
|
3558
|
+
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3559
|
+
return def;
|
|
3560
|
+
}
|
|
3561
|
+
case "openai": {
|
|
3562
|
+
if (agent.model === void 0) {
|
|
3563
|
+
throw new Error("migrateAgentConfig: agent.backend='openai' requires agent.model");
|
|
3564
|
+
}
|
|
3565
|
+
const def = { type: "openai", model: agent.model };
|
|
3566
|
+
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3567
|
+
return def;
|
|
3568
|
+
}
|
|
3569
|
+
case "gemini": {
|
|
3570
|
+
if (agent.model === void 0) {
|
|
3571
|
+
throw new Error("migrateAgentConfig: agent.backend='gemini' requires agent.model");
|
|
3572
|
+
}
|
|
3573
|
+
const def = { type: "gemini", model: agent.model };
|
|
3574
|
+
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3575
|
+
return def;
|
|
3576
|
+
}
|
|
3577
|
+
case "local": {
|
|
3578
|
+
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3579
|
+
throw new Error(
|
|
3580
|
+
"migrateAgentConfig: agent.backend='local' requires agent.localEndpoint and agent.localModel"
|
|
3581
|
+
);
|
|
3582
|
+
}
|
|
3583
|
+
const def = {
|
|
3584
|
+
type: "local",
|
|
3585
|
+
endpoint: agent.localEndpoint,
|
|
3586
|
+
model: agent.localModel
|
|
3587
|
+
};
|
|
3588
|
+
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3589
|
+
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3590
|
+
if (agent.localProbeIntervalMs !== void 0)
|
|
3591
|
+
def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3592
|
+
return def;
|
|
3593
|
+
}
|
|
3594
|
+
case "pi": {
|
|
3595
|
+
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3596
|
+
throw new Error(
|
|
3597
|
+
"migrateAgentConfig: agent.backend='pi' requires agent.localEndpoint and agent.localModel"
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
const def = {
|
|
3601
|
+
type: "pi",
|
|
3602
|
+
endpoint: agent.localEndpoint,
|
|
3603
|
+
model: agent.localModel
|
|
3604
|
+
};
|
|
3605
|
+
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3606
|
+
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3607
|
+
if (agent.localProbeIntervalMs !== void 0)
|
|
3608
|
+
def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3609
|
+
return def;
|
|
3610
|
+
}
|
|
3611
|
+
default:
|
|
3612
|
+
throw new Error(
|
|
3613
|
+
`migrateAgentConfig: unknown legacy backend '${String(backend)}'. Expected one of: mock, claude, anthropic, openai, gemini, local, pi.`
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
function synthesizeLocal(agent) {
|
|
3618
|
+
if (agent.localBackend === void 0) {
|
|
3619
|
+
throw new Error("synthesizeLocal called without agent.localBackend");
|
|
3620
|
+
}
|
|
3621
|
+
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3622
|
+
throw new Error(
|
|
3623
|
+
"migrateAgentConfig: agent.localBackend requires agent.localEndpoint and agent.localModel"
|
|
3624
|
+
);
|
|
3625
|
+
}
|
|
3626
|
+
if (agent.localBackend === "pi") {
|
|
3627
|
+
const def2 = {
|
|
3628
|
+
type: "pi",
|
|
3629
|
+
endpoint: agent.localEndpoint,
|
|
3630
|
+
model: agent.localModel
|
|
3631
|
+
};
|
|
3632
|
+
if (agent.localApiKey !== void 0) def2.apiKey = agent.localApiKey;
|
|
3633
|
+
if (agent.localTimeoutMs !== void 0) def2.timeoutMs = agent.localTimeoutMs;
|
|
3634
|
+
if (agent.localProbeIntervalMs !== void 0) def2.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3635
|
+
return def2;
|
|
3636
|
+
}
|
|
3637
|
+
const def = {
|
|
3638
|
+
type: "local",
|
|
3639
|
+
endpoint: agent.localEndpoint,
|
|
3640
|
+
model: agent.localModel
|
|
3641
|
+
};
|
|
3642
|
+
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3643
|
+
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3644
|
+
if (agent.localProbeIntervalMs !== void 0) def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3645
|
+
return def;
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
// src/agent/backend-router.ts
|
|
3649
|
+
var BackendRouter = class {
|
|
3650
|
+
backends;
|
|
3651
|
+
routing;
|
|
3652
|
+
constructor(opts) {
|
|
3653
|
+
this.backends = opts.backends;
|
|
3654
|
+
this.routing = opts.routing;
|
|
3655
|
+
this.validateReferences();
|
|
3656
|
+
}
|
|
3657
|
+
/**
|
|
3658
|
+
* Returns the backend name for a given use case.
|
|
3659
|
+
*
|
|
3660
|
+
* - `tier`: per-tier override, falling back to `routing.default`.
|
|
3661
|
+
* - `intelligence`: per-layer override under `routing.intelligence`,
|
|
3662
|
+
* falling back to `routing.default`.
|
|
3663
|
+
* - `maintenance` / `chat`: always `routing.default`.
|
|
3664
|
+
*/
|
|
3665
|
+
resolve(useCase) {
|
|
3666
|
+
switch (useCase.kind) {
|
|
3667
|
+
case "tier": {
|
|
3668
|
+
const named = this.routing[useCase.tier];
|
|
3669
|
+
return named ?? this.routing.default;
|
|
3670
|
+
}
|
|
3671
|
+
case "intelligence": {
|
|
3672
|
+
const intel = this.routing.intelligence;
|
|
3673
|
+
return intel?.[useCase.layer] ?? this.routing.default;
|
|
3674
|
+
}
|
|
3675
|
+
case "maintenance":
|
|
3676
|
+
case "chat":
|
|
3677
|
+
return this.routing.default;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3681
|
+
* Returns the BackendDef reference for the resolved name. Returns the
|
|
3682
|
+
* exact reference held in `backends` (no copy) so identity comparisons
|
|
3683
|
+
* succeed (SC21).
|
|
3684
|
+
*/
|
|
3685
|
+
resolveDefinition(useCase) {
|
|
3686
|
+
const name = this.resolve(useCase);
|
|
3687
|
+
const def = this.backends[name];
|
|
3688
|
+
if (!def) {
|
|
3689
|
+
throw new Error(
|
|
3690
|
+
`BackendRouter.resolveDefinition: routing target '${name}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
return def;
|
|
3694
|
+
}
|
|
3695
|
+
validateReferences() {
|
|
3696
|
+
const known = new Set(Object.keys(this.backends));
|
|
3697
|
+
const missing = [];
|
|
3698
|
+
const check = (path16, name) => {
|
|
3699
|
+
if (name !== void 0 && !known.has(name)) missing.push({ path: path16, name });
|
|
3700
|
+
};
|
|
3701
|
+
check("default", this.routing.default);
|
|
3702
|
+
check("quick-fix", this.routing["quick-fix"]);
|
|
3703
|
+
check("guided-change", this.routing["guided-change"]);
|
|
3704
|
+
check("full-exploration", this.routing["full-exploration"]);
|
|
3705
|
+
check("diagnostic", this.routing.diagnostic);
|
|
3706
|
+
check("intelligence.sel", this.routing.intelligence?.sel);
|
|
3707
|
+
check("intelligence.pesl", this.routing.intelligence?.pesl);
|
|
3708
|
+
if (missing.length > 0) {
|
|
3709
|
+
const detail = missing.map(({ path: path16, name }) => `routing.${path16} -> '${name}'`).join("; ");
|
|
3710
|
+
const known_ = [...known].join(", ") || "(none)";
|
|
3711
|
+
throw new Error(
|
|
3712
|
+
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
};
|
|
3717
|
+
|
|
3183
3718
|
// src/agent/backends/claude.ts
|
|
3184
3719
|
import { spawn as spawn2 } from "child_process";
|
|
3185
3720
|
import * as readline from "readline";
|
|
@@ -3535,12 +4070,125 @@ var ClaudeBackend = class {
|
|
|
3535
4070
|
}
|
|
3536
4071
|
};
|
|
3537
4072
|
|
|
3538
|
-
// src/agent/backends/
|
|
3539
|
-
import
|
|
4073
|
+
// src/agent/backends/anthropic.ts
|
|
4074
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3540
4075
|
import {
|
|
3541
4076
|
Ok as Ok10,
|
|
3542
4077
|
Err as Err7
|
|
3543
4078
|
} from "@harness-engineering/types";
|
|
4079
|
+
import { AnthropicCacheAdapter } from "@harness-engineering/core";
|
|
4080
|
+
var AnthropicBackend = class {
|
|
4081
|
+
name = "anthropic";
|
|
4082
|
+
config;
|
|
4083
|
+
client;
|
|
4084
|
+
cacheAdapter;
|
|
4085
|
+
constructor(config = {}) {
|
|
4086
|
+
this.config = {
|
|
4087
|
+
model: config.model ?? "claude-sonnet-4-20250514",
|
|
4088
|
+
apiKey: config.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "",
|
|
4089
|
+
maxTokens: config.maxTokens ?? 4096
|
|
4090
|
+
};
|
|
4091
|
+
this.client = new Anthropic({ apiKey: this.config.apiKey });
|
|
4092
|
+
this.cacheAdapter = new AnthropicCacheAdapter();
|
|
4093
|
+
}
|
|
4094
|
+
async startSession(params) {
|
|
4095
|
+
if (!this.config.apiKey) {
|
|
4096
|
+
return Err7({
|
|
4097
|
+
category: "agent_not_found",
|
|
4098
|
+
message: "ANTHROPIC_API_KEY is not set"
|
|
4099
|
+
});
|
|
4100
|
+
}
|
|
4101
|
+
const session = {
|
|
4102
|
+
sessionId: `anthropic-session-${Date.now()}`,
|
|
4103
|
+
workspacePath: params.workspacePath,
|
|
4104
|
+
backendName: this.name,
|
|
4105
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4106
|
+
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4107
|
+
};
|
|
4108
|
+
return Ok10(session);
|
|
4109
|
+
}
|
|
4110
|
+
async *runTurn(session, params) {
|
|
4111
|
+
const anthropicSession = session;
|
|
4112
|
+
const systemBlocks = anthropicSession.systemPrompt ? [
|
|
4113
|
+
this.cacheAdapter.wrapSystemBlock(
|
|
4114
|
+
anthropicSession.systemPrompt,
|
|
4115
|
+
"session"
|
|
4116
|
+
)
|
|
4117
|
+
] : void 0;
|
|
4118
|
+
try {
|
|
4119
|
+
const stream = this.client.messages.stream({
|
|
4120
|
+
model: this.config.model,
|
|
4121
|
+
max_tokens: this.config.maxTokens,
|
|
4122
|
+
...systemBlocks && { system: systemBlocks },
|
|
4123
|
+
messages: [{ role: "user", content: params.prompt }]
|
|
4124
|
+
});
|
|
4125
|
+
for await (const event of stream) {
|
|
4126
|
+
if (event.type === "content_block_delta" && "text" in event.delta) {
|
|
4127
|
+
yield {
|
|
4128
|
+
type: "text",
|
|
4129
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4130
|
+
content: event.delta.text,
|
|
4131
|
+
sessionId: session.sessionId
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
const finalMessage = await stream.finalMessage();
|
|
4136
|
+
const { input_tokens, output_tokens } = finalMessage.usage;
|
|
4137
|
+
const cacheUsage = this.cacheAdapter.parseCacheUsage(finalMessage);
|
|
4138
|
+
const usage = {
|
|
4139
|
+
inputTokens: input_tokens,
|
|
4140
|
+
outputTokens: output_tokens,
|
|
4141
|
+
totalTokens: input_tokens + output_tokens,
|
|
4142
|
+
cacheCreationTokens: cacheUsage.cacheCreationTokens,
|
|
4143
|
+
cacheReadTokens: cacheUsage.cacheReadTokens
|
|
4144
|
+
};
|
|
4145
|
+
yield {
|
|
4146
|
+
type: "usage",
|
|
4147
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4148
|
+
sessionId: session.sessionId,
|
|
4149
|
+
usage
|
|
4150
|
+
};
|
|
4151
|
+
return {
|
|
4152
|
+
success: true,
|
|
4153
|
+
sessionId: session.sessionId,
|
|
4154
|
+
usage
|
|
4155
|
+
};
|
|
4156
|
+
} catch (err) {
|
|
4157
|
+
const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
|
|
4158
|
+
yield {
|
|
4159
|
+
type: "error",
|
|
4160
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4161
|
+
content: errorMessage,
|
|
4162
|
+
sessionId: session.sessionId
|
|
4163
|
+
};
|
|
4164
|
+
return {
|
|
4165
|
+
success: false,
|
|
4166
|
+
sessionId: session.sessionId,
|
|
4167
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
4168
|
+
error: errorMessage
|
|
4169
|
+
};
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
async stopSession(_session) {
|
|
4173
|
+
return Ok10(void 0);
|
|
4174
|
+
}
|
|
4175
|
+
async healthCheck() {
|
|
4176
|
+
if (!this.config.apiKey) {
|
|
4177
|
+
return Err7({
|
|
4178
|
+
category: "response_error",
|
|
4179
|
+
message: "ANTHROPIC_API_KEY is not set"
|
|
4180
|
+
});
|
|
4181
|
+
}
|
|
4182
|
+
return Ok10(void 0);
|
|
4183
|
+
}
|
|
4184
|
+
};
|
|
4185
|
+
|
|
4186
|
+
// src/agent/backends/openai.ts
|
|
4187
|
+
import OpenAI from "openai";
|
|
4188
|
+
import {
|
|
4189
|
+
Ok as Ok11,
|
|
4190
|
+
Err as Err8
|
|
4191
|
+
} from "@harness-engineering/types";
|
|
3544
4192
|
import { OpenAICacheAdapter } from "@harness-engineering/core";
|
|
3545
4193
|
var OpenAIBackend = class {
|
|
3546
4194
|
name = "openai";
|
|
@@ -3557,7 +4205,7 @@ var OpenAIBackend = class {
|
|
|
3557
4205
|
}
|
|
3558
4206
|
async startSession(params) {
|
|
3559
4207
|
if (!this.config.apiKey) {
|
|
3560
|
-
return
|
|
4208
|
+
return Err8({
|
|
3561
4209
|
category: "agent_not_found",
|
|
3562
4210
|
message: "OPENAI_API_KEY is not set"
|
|
3563
4211
|
});
|
|
@@ -3569,7 +4217,7 @@ var OpenAIBackend = class {
|
|
|
3569
4217
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3570
4218
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
3571
4219
|
};
|
|
3572
|
-
return
|
|
4220
|
+
return Ok11(session);
|
|
3573
4221
|
}
|
|
3574
4222
|
async *runTurn(session, params) {
|
|
3575
4223
|
const openAISession = session;
|
|
@@ -3645,14 +4293,14 @@ var OpenAIBackend = class {
|
|
|
3645
4293
|
};
|
|
3646
4294
|
}
|
|
3647
4295
|
async stopSession(_session) {
|
|
3648
|
-
return
|
|
4296
|
+
return Ok11(void 0);
|
|
3649
4297
|
}
|
|
3650
4298
|
async healthCheck() {
|
|
3651
4299
|
try {
|
|
3652
4300
|
await this.client.models.list();
|
|
3653
|
-
return
|
|
4301
|
+
return Ok11(void 0);
|
|
3654
4302
|
} catch (err) {
|
|
3655
|
-
return
|
|
4303
|
+
return Err8({
|
|
3656
4304
|
category: "response_error",
|
|
3657
4305
|
message: err instanceof Error ? err.message : "OpenAI health check failed"
|
|
3658
4306
|
});
|
|
@@ -3663,8 +4311,8 @@ var OpenAIBackend = class {
|
|
|
3663
4311
|
// src/agent/backends/gemini.ts
|
|
3664
4312
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
3665
4313
|
import {
|
|
3666
|
-
Ok as
|
|
3667
|
-
Err as
|
|
4314
|
+
Ok as Ok12,
|
|
4315
|
+
Err as Err9
|
|
3668
4316
|
} from "@harness-engineering/types";
|
|
3669
4317
|
import { GeminiCacheAdapter } from "@harness-engineering/core";
|
|
3670
4318
|
var GeminiBackend = class {
|
|
@@ -3680,7 +4328,7 @@ var GeminiBackend = class {
|
|
|
3680
4328
|
}
|
|
3681
4329
|
async startSession(params) {
|
|
3682
4330
|
if (!this.config.apiKey) {
|
|
3683
|
-
return
|
|
4331
|
+
return Err9({
|
|
3684
4332
|
category: "agent_not_found",
|
|
3685
4333
|
message: "GEMINI_API_KEY is not set"
|
|
3686
4334
|
});
|
|
@@ -3692,7 +4340,7 @@ var GeminiBackend = class {
|
|
|
3692
4340
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3693
4341
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
3694
4342
|
};
|
|
3695
|
-
return
|
|
4343
|
+
return Ok12(session);
|
|
3696
4344
|
}
|
|
3697
4345
|
async *runTurn(session, params) {
|
|
3698
4346
|
const geminiSession = session;
|
|
@@ -3762,135 +4410,22 @@ var GeminiBackend = class {
|
|
|
3762
4410
|
success: true,
|
|
3763
4411
|
sessionId: session.sessionId,
|
|
3764
4412
|
usage
|
|
3765
|
-
};
|
|
3766
|
-
}
|
|
3767
|
-
async stopSession(_session) {
|
|
3768
|
-
return Ok11(void 0);
|
|
3769
|
-
}
|
|
3770
|
-
async healthCheck() {
|
|
3771
|
-
try {
|
|
3772
|
-
const genAI = new GoogleGenerativeAI(this.config.apiKey);
|
|
3773
|
-
genAI.getGenerativeModel({ model: this.config.model });
|
|
3774
|
-
return Ok11(void 0);
|
|
3775
|
-
} catch (err) {
|
|
3776
|
-
return Err8({
|
|
3777
|
-
category: "response_error",
|
|
3778
|
-
message: err instanceof Error ? err.message : "Gemini health check failed"
|
|
3779
|
-
});
|
|
3780
|
-
}
|
|
3781
|
-
}
|
|
3782
|
-
};
|
|
3783
|
-
|
|
3784
|
-
// src/agent/backends/anthropic.ts
|
|
3785
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
3786
|
-
import {
|
|
3787
|
-
Ok as Ok12,
|
|
3788
|
-
Err as Err9
|
|
3789
|
-
} from "@harness-engineering/types";
|
|
3790
|
-
import { AnthropicCacheAdapter } from "@harness-engineering/core";
|
|
3791
|
-
var AnthropicBackend = class {
|
|
3792
|
-
name = "anthropic";
|
|
3793
|
-
config;
|
|
3794
|
-
client;
|
|
3795
|
-
cacheAdapter;
|
|
3796
|
-
constructor(config = {}) {
|
|
3797
|
-
this.config = {
|
|
3798
|
-
model: config.model ?? "claude-sonnet-4-20250514",
|
|
3799
|
-
apiKey: config.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "",
|
|
3800
|
-
maxTokens: config.maxTokens ?? 4096
|
|
3801
|
-
};
|
|
3802
|
-
this.client = new Anthropic({ apiKey: this.config.apiKey });
|
|
3803
|
-
this.cacheAdapter = new AnthropicCacheAdapter();
|
|
3804
|
-
}
|
|
3805
|
-
async startSession(params) {
|
|
3806
|
-
if (!this.config.apiKey) {
|
|
3807
|
-
return Err9({
|
|
3808
|
-
category: "agent_not_found",
|
|
3809
|
-
message: "ANTHROPIC_API_KEY is not set"
|
|
3810
|
-
});
|
|
3811
|
-
}
|
|
3812
|
-
const session = {
|
|
3813
|
-
sessionId: `anthropic-session-${Date.now()}`,
|
|
3814
|
-
workspacePath: params.workspacePath,
|
|
3815
|
-
backendName: this.name,
|
|
3816
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3817
|
-
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
3818
|
-
};
|
|
3819
|
-
return Ok12(session);
|
|
3820
|
-
}
|
|
3821
|
-
async *runTurn(session, params) {
|
|
3822
|
-
const anthropicSession = session;
|
|
3823
|
-
const systemBlocks = anthropicSession.systemPrompt ? [
|
|
3824
|
-
this.cacheAdapter.wrapSystemBlock(
|
|
3825
|
-
anthropicSession.systemPrompt,
|
|
3826
|
-
"session"
|
|
3827
|
-
)
|
|
3828
|
-
] : void 0;
|
|
3829
|
-
try {
|
|
3830
|
-
const stream = this.client.messages.stream({
|
|
3831
|
-
model: this.config.model,
|
|
3832
|
-
max_tokens: this.config.maxTokens,
|
|
3833
|
-
...systemBlocks && { system: systemBlocks },
|
|
3834
|
-
messages: [{ role: "user", content: params.prompt }]
|
|
3835
|
-
});
|
|
3836
|
-
for await (const event of stream) {
|
|
3837
|
-
if (event.type === "content_block_delta" && "text" in event.delta) {
|
|
3838
|
-
yield {
|
|
3839
|
-
type: "text",
|
|
3840
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3841
|
-
content: event.delta.text,
|
|
3842
|
-
sessionId: session.sessionId
|
|
3843
|
-
};
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
const finalMessage = await stream.finalMessage();
|
|
3847
|
-
const { input_tokens, output_tokens } = finalMessage.usage;
|
|
3848
|
-
const cacheUsage = this.cacheAdapter.parseCacheUsage(finalMessage);
|
|
3849
|
-
const usage = {
|
|
3850
|
-
inputTokens: input_tokens,
|
|
3851
|
-
outputTokens: output_tokens,
|
|
3852
|
-
totalTokens: input_tokens + output_tokens,
|
|
3853
|
-
cacheCreationTokens: cacheUsage.cacheCreationTokens,
|
|
3854
|
-
cacheReadTokens: cacheUsage.cacheReadTokens
|
|
3855
|
-
};
|
|
3856
|
-
yield {
|
|
3857
|
-
type: "usage",
|
|
3858
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3859
|
-
sessionId: session.sessionId,
|
|
3860
|
-
usage
|
|
3861
|
-
};
|
|
3862
|
-
return {
|
|
3863
|
-
success: true,
|
|
3864
|
-
sessionId: session.sessionId,
|
|
3865
|
-
usage
|
|
3866
|
-
};
|
|
3867
|
-
} catch (err) {
|
|
3868
|
-
const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
|
|
3869
|
-
yield {
|
|
3870
|
-
type: "error",
|
|
3871
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3872
|
-
content: errorMessage,
|
|
3873
|
-
sessionId: session.sessionId
|
|
3874
|
-
};
|
|
3875
|
-
return {
|
|
3876
|
-
success: false,
|
|
3877
|
-
sessionId: session.sessionId,
|
|
3878
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
3879
|
-
error: errorMessage
|
|
3880
|
-
};
|
|
3881
|
-
}
|
|
4413
|
+
};
|
|
3882
4414
|
}
|
|
3883
4415
|
async stopSession(_session) {
|
|
3884
4416
|
return Ok12(void 0);
|
|
3885
4417
|
}
|
|
3886
4418
|
async healthCheck() {
|
|
3887
|
-
|
|
4419
|
+
try {
|
|
4420
|
+
const genAI = new GoogleGenerativeAI(this.config.apiKey);
|
|
4421
|
+
genAI.getGenerativeModel({ model: this.config.model });
|
|
4422
|
+
return Ok12(void 0);
|
|
4423
|
+
} catch (err) {
|
|
3888
4424
|
return Err9({
|
|
3889
4425
|
category: "response_error",
|
|
3890
|
-
message: "
|
|
4426
|
+
message: err instanceof Error ? err.message : "Gemini health check failed"
|
|
3891
4427
|
});
|
|
3892
4428
|
}
|
|
3893
|
-
return Ok12(void 0);
|
|
3894
4429
|
}
|
|
3895
4430
|
};
|
|
3896
4431
|
|
|
@@ -3904,6 +4439,7 @@ var DEFAULT_TIMEOUT_MS = 9e4;
|
|
|
3904
4439
|
var LocalBackend = class {
|
|
3905
4440
|
name = "local";
|
|
3906
4441
|
config;
|
|
4442
|
+
getModel;
|
|
3907
4443
|
client;
|
|
3908
4444
|
constructor(config = {}) {
|
|
3909
4445
|
this.config = {
|
|
@@ -3912,6 +4448,7 @@ var LocalBackend = class {
|
|
|
3912
4448
|
apiKey: config.apiKey ?? "ollama",
|
|
3913
4449
|
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
3914
4450
|
};
|
|
4451
|
+
this.getModel = config.getModel;
|
|
3915
4452
|
this.client = new OpenAI2({
|
|
3916
4453
|
apiKey: this.config.apiKey,
|
|
3917
4454
|
baseURL: this.config.endpoint,
|
|
@@ -3919,11 +4456,25 @@ var LocalBackend = class {
|
|
|
3919
4456
|
});
|
|
3920
4457
|
}
|
|
3921
4458
|
async startSession(params) {
|
|
4459
|
+
let resolvedModel;
|
|
4460
|
+
if (this.getModel) {
|
|
4461
|
+
const candidate = this.getModel();
|
|
4462
|
+
if (candidate === null) {
|
|
4463
|
+
return Err10({
|
|
4464
|
+
category: "agent_not_found",
|
|
4465
|
+
message: "No local model available; check dashboard for details."
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
resolvedModel = candidate;
|
|
4469
|
+
} else {
|
|
4470
|
+
resolvedModel = this.config.model;
|
|
4471
|
+
}
|
|
3922
4472
|
const session = {
|
|
3923
4473
|
sessionId: `local-session-${Date.now()}`,
|
|
3924
4474
|
workspacePath: params.workspacePath,
|
|
3925
4475
|
backendName: this.name,
|
|
3926
4476
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4477
|
+
resolvedModel,
|
|
3927
4478
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
3928
4479
|
};
|
|
3929
4480
|
return Ok13(session);
|
|
@@ -3940,7 +4491,7 @@ var LocalBackend = class {
|
|
|
3940
4491
|
let totalTokens = 0;
|
|
3941
4492
|
try {
|
|
3942
4493
|
const stream = await this.client.chat.completions.create({
|
|
3943
|
-
model:
|
|
4494
|
+
model: localSession.resolvedModel,
|
|
3944
4495
|
messages,
|
|
3945
4496
|
stream: true,
|
|
3946
4497
|
stream_options: { include_usage: true }
|
|
@@ -4108,13 +4659,41 @@ function buildLocalModel(config) {
|
|
|
4108
4659
|
var PiBackend = class {
|
|
4109
4660
|
name = "pi";
|
|
4110
4661
|
config;
|
|
4662
|
+
/**
|
|
4663
|
+
* Per-request timeout in ms (default 90_000). Spec 2 P2-I1: enforced at
|
|
4664
|
+
* the request boundary by `runTurn` racing `piSession.prompt()` against
|
|
4665
|
+
* an `AbortController + setTimeout(timeoutMs)`. On timeout the
|
|
4666
|
+
* underlying pi session is aborted and the turn returns a failed
|
|
4667
|
+
* `TurnResult` carrying a timeout-tagged error message. Setting
|
|
4668
|
+
* `timeoutMs: 0` disables the watchdog (preserves the pre-fix-up
|
|
4669
|
+
* "no enforcement" behavior for callers that want the SDK default).
|
|
4670
|
+
*/
|
|
4671
|
+
timeoutMs;
|
|
4111
4672
|
constructor(config = {}) {
|
|
4112
4673
|
this.config = config;
|
|
4674
|
+
this.timeoutMs = config.timeoutMs ?? 9e4;
|
|
4113
4675
|
}
|
|
4114
4676
|
async startSession(params) {
|
|
4115
4677
|
try {
|
|
4678
|
+
let resolvedModelName;
|
|
4679
|
+
if (this.config.getModel) {
|
|
4680
|
+
const candidate = this.config.getModel();
|
|
4681
|
+
if (candidate === null) {
|
|
4682
|
+
return Err11({
|
|
4683
|
+
category: "agent_not_found",
|
|
4684
|
+
message: "No local model available; check dashboard for details."
|
|
4685
|
+
});
|
|
4686
|
+
}
|
|
4687
|
+
resolvedModelName = candidate;
|
|
4688
|
+
} else {
|
|
4689
|
+
resolvedModelName = this.config.model;
|
|
4690
|
+
}
|
|
4116
4691
|
const piSdk = await import("@mariozechner/pi-coding-agent");
|
|
4117
|
-
const model = buildLocalModel(
|
|
4692
|
+
const model = buildLocalModel({
|
|
4693
|
+
model: resolvedModelName,
|
|
4694
|
+
endpoint: this.config.endpoint,
|
|
4695
|
+
apiKey: this.config.apiKey
|
|
4696
|
+
});
|
|
4118
4697
|
const { session: piSession } = await piSdk.createAgentSession({
|
|
4119
4698
|
cwd: params.workspacePath,
|
|
4120
4699
|
...model !== void 0 && { model },
|
|
@@ -4154,15 +4733,45 @@ var PiBackend = class {
|
|
|
4154
4733
|
signal();
|
|
4155
4734
|
});
|
|
4156
4735
|
session.unsubscribe = unsubscribe;
|
|
4157
|
-
|
|
4158
|
-
|
|
4736
|
+
let timeoutHandle = null;
|
|
4737
|
+
let timedOut = false;
|
|
4738
|
+
if (this.timeoutMs > 0) {
|
|
4739
|
+
timeoutHandle = setTimeout(() => {
|
|
4740
|
+
timedOut = true;
|
|
4741
|
+
promptErrorMsg = `Pi backend request timed out after ${this.timeoutMs}ms`;
|
|
4159
4742
|
promptDone = true;
|
|
4743
|
+
try {
|
|
4744
|
+
const maybeAbort = piSession.abort?.();
|
|
4745
|
+
if (maybeAbort && typeof maybeAbort.catch === "function") {
|
|
4746
|
+
maybeAbort.catch(() => {
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
} catch {
|
|
4750
|
+
}
|
|
4160
4751
|
signal();
|
|
4752
|
+
}, this.timeoutMs);
|
|
4753
|
+
}
|
|
4754
|
+
const clearTimeoutHandle = () => {
|
|
4755
|
+
if (timeoutHandle !== null) {
|
|
4756
|
+
clearTimeout(timeoutHandle);
|
|
4757
|
+
timeoutHandle = null;
|
|
4758
|
+
}
|
|
4759
|
+
};
|
|
4760
|
+
const promptPromise = piSession.prompt(params.prompt).then(
|
|
4761
|
+
() => {
|
|
4762
|
+
if (!timedOut) {
|
|
4763
|
+
clearTimeoutHandle();
|
|
4764
|
+
promptDone = true;
|
|
4765
|
+
signal();
|
|
4766
|
+
}
|
|
4161
4767
|
},
|
|
4162
4768
|
(err) => {
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4769
|
+
if (!timedOut) {
|
|
4770
|
+
clearTimeoutHandle();
|
|
4771
|
+
promptErrorMsg = err.message;
|
|
4772
|
+
promptDone = true;
|
|
4773
|
+
signal();
|
|
4774
|
+
}
|
|
4166
4775
|
}
|
|
4167
4776
|
);
|
|
4168
4777
|
let inputTokens = 0;
|
|
@@ -4178,12 +4787,15 @@ var PiBackend = class {
|
|
|
4178
4787
|
})
|
|
4179
4788
|
});
|
|
4180
4789
|
} finally {
|
|
4790
|
+
clearTimeoutHandle();
|
|
4181
4791
|
resolveWait?.();
|
|
4182
4792
|
resolveWait = null;
|
|
4183
4793
|
unsubscribe();
|
|
4184
4794
|
session.unsubscribe = null;
|
|
4185
|
-
|
|
4186
|
-
|
|
4795
|
+
if (!timedOut) {
|
|
4796
|
+
await promptPromise.catch(() => {
|
|
4797
|
+
});
|
|
4798
|
+
}
|
|
4187
4799
|
}
|
|
4188
4800
|
const totalTokens = inputTokens + outputTokens;
|
|
4189
4801
|
if (promptErrorMsg) {
|
|
@@ -4243,6 +4855,60 @@ var PiBackend = class {
|
|
|
4243
4855
|
}
|
|
4244
4856
|
};
|
|
4245
4857
|
|
|
4858
|
+
// src/agent/backend-factory.ts
|
|
4859
|
+
function makeGetModel(model) {
|
|
4860
|
+
if (typeof model === "string") return () => model;
|
|
4861
|
+
if (Array.isArray(model) && model.length > 0) return () => model[0] ?? null;
|
|
4862
|
+
return () => null;
|
|
4863
|
+
}
|
|
4864
|
+
function createBackend(def) {
|
|
4865
|
+
switch (def.type) {
|
|
4866
|
+
case "mock":
|
|
4867
|
+
return new MockBackend();
|
|
4868
|
+
case "claude":
|
|
4869
|
+
return new ClaudeBackend(def.command ?? "claude");
|
|
4870
|
+
case "anthropic":
|
|
4871
|
+
return new AnthropicBackend({
|
|
4872
|
+
model: def.model,
|
|
4873
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
|
|
4874
|
+
});
|
|
4875
|
+
case "openai":
|
|
4876
|
+
return new OpenAIBackend({
|
|
4877
|
+
model: def.model,
|
|
4878
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
|
|
4879
|
+
});
|
|
4880
|
+
case "gemini":
|
|
4881
|
+
return new GeminiBackend({
|
|
4882
|
+
model: def.model,
|
|
4883
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
|
|
4884
|
+
});
|
|
4885
|
+
case "local": {
|
|
4886
|
+
const isArray = Array.isArray(def.model);
|
|
4887
|
+
return new LocalBackend({
|
|
4888
|
+
endpoint: def.endpoint,
|
|
4889
|
+
...typeof def.model === "string" ? { model: def.model } : {},
|
|
4890
|
+
...isArray ? { getModel: makeGetModel(def.model) } : {},
|
|
4891
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
|
|
4892
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
4893
|
+
});
|
|
4894
|
+
}
|
|
4895
|
+
case "pi": {
|
|
4896
|
+
const isArray = Array.isArray(def.model);
|
|
4897
|
+
return new PiBackend({
|
|
4898
|
+
endpoint: def.endpoint,
|
|
4899
|
+
...typeof def.model === "string" ? { model: def.model } : {},
|
|
4900
|
+
...isArray ? { getModel: makeGetModel(def.model) } : {},
|
|
4901
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
|
|
4902
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
4903
|
+
});
|
|
4904
|
+
}
|
|
4905
|
+
default: {
|
|
4906
|
+
const exhaustive = def;
|
|
4907
|
+
throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4246
4912
|
// src/agent/backends/container.ts
|
|
4247
4913
|
import {
|
|
4248
4914
|
Err as Err12
|
|
@@ -4606,6 +5272,195 @@ function createSecretBackend(config) {
|
|
|
4606
5272
|
}
|
|
4607
5273
|
}
|
|
4608
5274
|
|
|
5275
|
+
// src/agent/orchestrator-backend-factory.ts
|
|
5276
|
+
var OrchestratorBackendFactory = class {
|
|
5277
|
+
router;
|
|
5278
|
+
opts;
|
|
5279
|
+
constructor(opts) {
|
|
5280
|
+
this.opts = opts;
|
|
5281
|
+
this.router = new BackendRouter({ backends: opts.backends, routing: opts.routing });
|
|
5282
|
+
}
|
|
5283
|
+
/**
|
|
5284
|
+
* Resolve `useCase` to a backend name, materialize a fresh
|
|
5285
|
+
* `AgentBackend`, optionally rebind its model resolver, and apply
|
|
5286
|
+
* sandbox wrapping. Idempotent across calls (no caching) — the AgentRunner
|
|
5287
|
+
* holds the per-dispatch reference and discards it when the run ends.
|
|
5288
|
+
*/
|
|
5289
|
+
/**
|
|
5290
|
+
* Resolve `useCase` to its routed backend name, exposing the
|
|
5291
|
+
* router lookup without materializing a backend. Used by callers
|
|
5292
|
+
* (e.g., the orchestrator's dispatch site) that need to label
|
|
5293
|
+
* telemetry with the routed name BEFORE constructing the backend.
|
|
5294
|
+
*
|
|
5295
|
+
* Spec 2 P2-I2: previously the orchestrator labelled `LiveSession`
|
|
5296
|
+
* + `StreamRecorder` with the legacy `agent.backend` field, which
|
|
5297
|
+
* is `undefined` for pure-modern configs. Threading the routed name
|
|
5298
|
+
* through dispatch eliminates that gap.
|
|
5299
|
+
*/
|
|
5300
|
+
resolveName(useCase) {
|
|
5301
|
+
return this.router.resolve(useCase);
|
|
5302
|
+
}
|
|
5303
|
+
forUseCase(useCase) {
|
|
5304
|
+
const def = this.router.resolveDefinition(useCase);
|
|
5305
|
+
const name = this.router.resolve(useCase);
|
|
5306
|
+
let backend;
|
|
5307
|
+
if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
|
|
5308
|
+
const getModel = this.opts.getResolverModelFor(name);
|
|
5309
|
+
backend = getModel ? this.buildLocalLikeWithResolver(def, getModel) : createBackend(def);
|
|
5310
|
+
} else {
|
|
5311
|
+
backend = createBackend(def);
|
|
5312
|
+
}
|
|
5313
|
+
if (this.opts.sandboxPolicy === "docker" && this.opts.container) {
|
|
5314
|
+
backend = this.wrapInContainer(backend);
|
|
5315
|
+
}
|
|
5316
|
+
return backend;
|
|
5317
|
+
}
|
|
5318
|
+
/**
|
|
5319
|
+
* Rebuild a `local`/`pi` backend with a resolver-bound `getModel`,
|
|
5320
|
+
* mirroring `createBackend`'s local/pi branches but substituting the
|
|
5321
|
+
* head-of-array placeholder with the orchestrator-owned resolver.
|
|
5322
|
+
*/
|
|
5323
|
+
buildLocalLikeWithResolver(def, getModel) {
|
|
5324
|
+
if (def.type === "local") {
|
|
5325
|
+
return new LocalBackend({
|
|
5326
|
+
endpoint: def.endpoint,
|
|
5327
|
+
getModel,
|
|
5328
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
|
|
5329
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
5330
|
+
});
|
|
5331
|
+
}
|
|
5332
|
+
if (def.type === "pi") {
|
|
5333
|
+
return new PiBackend({
|
|
5334
|
+
endpoint: def.endpoint,
|
|
5335
|
+
getModel,
|
|
5336
|
+
...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
|
|
5337
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
5338
|
+
});
|
|
5339
|
+
}
|
|
5340
|
+
throw new Error(
|
|
5341
|
+
`OrchestratorBackendFactory.buildLocalLikeWithResolver called with non-local def.type='${def.type}'`
|
|
5342
|
+
);
|
|
5343
|
+
}
|
|
5344
|
+
/**
|
|
5345
|
+
* Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret
|
|
5346
|
+
* backend per call so each dispatch sees a fresh container handle map
|
|
5347
|
+
* (ContainerBackend keeps its own per-instance Map<sessionId, handle>).
|
|
5348
|
+
*/
|
|
5349
|
+
wrapInContainer(inner) {
|
|
5350
|
+
const runtime = new DockerRuntime();
|
|
5351
|
+
const secretBackend = this.opts.secrets ? createSecretBackend(this.opts.secrets) : null;
|
|
5352
|
+
const secretKeys = this.opts.secrets?.keys ?? [];
|
|
5353
|
+
return new ContainerBackend(
|
|
5354
|
+
inner,
|
|
5355
|
+
runtime,
|
|
5356
|
+
secretBackend,
|
|
5357
|
+
this.opts.container,
|
|
5358
|
+
secretKeys
|
|
5359
|
+
);
|
|
5360
|
+
}
|
|
5361
|
+
};
|
|
5362
|
+
|
|
5363
|
+
// src/agent/analysis-provider-factory.ts
|
|
5364
|
+
import {
|
|
5365
|
+
AnthropicAnalysisProvider,
|
|
5366
|
+
ClaudeCliAnalysisProvider,
|
|
5367
|
+
OpenAICompatibleAnalysisProvider
|
|
5368
|
+
} from "@harness-engineering/intelligence";
|
|
5369
|
+
function buildAnalysisProvider(args) {
|
|
5370
|
+
const { def, backendName, layer, intelligence, logger } = args;
|
|
5371
|
+
const layerModel = layer === "sel" ? intelligence?.models?.sel : intelligence?.models?.pesl;
|
|
5372
|
+
switch (def.type) {
|
|
5373
|
+
case "local":
|
|
5374
|
+
case "pi":
|
|
5375
|
+
return buildLocalLikeProvider(def, args, layerModel);
|
|
5376
|
+
case "anthropic":
|
|
5377
|
+
return buildAnthropicProvider(def, args, layerModel);
|
|
5378
|
+
case "openai":
|
|
5379
|
+
return buildOpenAIProvider(def, args, layerModel);
|
|
5380
|
+
case "claude":
|
|
5381
|
+
return buildClaudeCliProvider(def, args, layerModel);
|
|
5382
|
+
case "mock":
|
|
5383
|
+
case "gemini":
|
|
5384
|
+
logger.warn(
|
|
5385
|
+
`Intelligence pipeline disabled for layer '${layer}': routed backend '${backendName}' has type '${def.type}' which has no AnalysisProvider implementation.`
|
|
5386
|
+
);
|
|
5387
|
+
return null;
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
function buildLocalLikeProvider(def, args, layerModel) {
|
|
5391
|
+
const { backendName, getResolverStatusSnapshot, intelligence, logger } = args;
|
|
5392
|
+
const snapshot = getResolverStatusSnapshot();
|
|
5393
|
+
if (!snapshot || !snapshot.available) {
|
|
5394
|
+
const configured = snapshot?.configured ?? [];
|
|
5395
|
+
const detected = snapshot?.detected ?? [];
|
|
5396
|
+
logger.warn(
|
|
5397
|
+
`Intelligence pipeline disabled for backend '${backendName}' at ${def.endpoint}: no configured local model loaded. Configured: [${configured.join(", ")}]. Detected: [${detected.join(", ")}].`
|
|
5398
|
+
);
|
|
5399
|
+
return null;
|
|
5400
|
+
}
|
|
5401
|
+
const model = layerModel ?? snapshot.resolved ?? void 0;
|
|
5402
|
+
const apiKey = def.apiKey ?? "ollama";
|
|
5403
|
+
logger.info(
|
|
5404
|
+
`Intelligence pipeline using backend '${backendName}' (${def.type}) at ${def.endpoint} (model: ${model ?? "(default)"})`
|
|
5405
|
+
);
|
|
5406
|
+
return new OpenAICompatibleAnalysisProvider({
|
|
5407
|
+
apiKey,
|
|
5408
|
+
baseUrl: def.endpoint,
|
|
5409
|
+
...model !== void 0 && { defaultModel: model },
|
|
5410
|
+
...intelligence?.requestTimeoutMs !== void 0 && {
|
|
5411
|
+
timeoutMs: intelligence.requestTimeoutMs
|
|
5412
|
+
},
|
|
5413
|
+
...intelligence?.promptSuffix !== void 0 && { promptSuffix: intelligence.promptSuffix },
|
|
5414
|
+
...intelligence?.jsonMode !== void 0 && { jsonMode: intelligence.jsonMode }
|
|
5415
|
+
});
|
|
5416
|
+
}
|
|
5417
|
+
function buildAnthropicProvider(def, args, layerModel) {
|
|
5418
|
+
const apiKey = def.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
5419
|
+
const model = layerModel ?? def.model;
|
|
5420
|
+
if (apiKey) {
|
|
5421
|
+
return new AnthropicAnalysisProvider({
|
|
5422
|
+
apiKey,
|
|
5423
|
+
...model !== void 0 && { defaultModel: model }
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5426
|
+
args.logger.info(
|
|
5427
|
+
`Intelligence pipeline routed to '${args.backendName}' (anthropic) without API key \u2014 using Claude CLI fallback.`
|
|
5428
|
+
);
|
|
5429
|
+
return new ClaudeCliAnalysisProvider({
|
|
5430
|
+
...model !== void 0 && { defaultModel: model },
|
|
5431
|
+
...args.intelligence?.requestTimeoutMs !== void 0 && {
|
|
5432
|
+
timeoutMs: args.intelligence.requestTimeoutMs
|
|
5433
|
+
}
|
|
5434
|
+
});
|
|
5435
|
+
}
|
|
5436
|
+
function buildOpenAIProvider(def, args, layerModel) {
|
|
5437
|
+
const apiKey = def.apiKey ?? process.env.OPENAI_API_KEY;
|
|
5438
|
+
if (!apiKey) {
|
|
5439
|
+
args.logger.warn(
|
|
5440
|
+
`Intelligence pipeline disabled for backend '${args.backendName}' (openai): no API key configured.`
|
|
5441
|
+
);
|
|
5442
|
+
return null;
|
|
5443
|
+
}
|
|
5444
|
+
const model = layerModel ?? def.model;
|
|
5445
|
+
return new OpenAICompatibleAnalysisProvider({
|
|
5446
|
+
apiKey,
|
|
5447
|
+
baseUrl: "https://api.openai.com/v1",
|
|
5448
|
+
...model !== void 0 && { defaultModel: model },
|
|
5449
|
+
...args.intelligence?.requestTimeoutMs !== void 0 && {
|
|
5450
|
+
timeoutMs: args.intelligence.requestTimeoutMs
|
|
5451
|
+
}
|
|
5452
|
+
});
|
|
5453
|
+
}
|
|
5454
|
+
function buildClaudeCliProvider(def, args, layerModel) {
|
|
5455
|
+
return new ClaudeCliAnalysisProvider({
|
|
5456
|
+
...def.command !== void 0 && { command: def.command },
|
|
5457
|
+
...layerModel !== void 0 && { defaultModel: layerModel },
|
|
5458
|
+
...args.intelligence?.requestTimeoutMs !== void 0 && {
|
|
5459
|
+
timeoutMs: args.intelligence.requestTimeoutMs
|
|
5460
|
+
}
|
|
5461
|
+
});
|
|
5462
|
+
}
|
|
5463
|
+
|
|
4609
5464
|
// src/server/http.ts
|
|
4610
5465
|
import * as http from "http";
|
|
4611
5466
|
import * as path13 from "path";
|
|
@@ -4665,7 +5520,7 @@ var WebSocketBroadcaster = class {
|
|
|
4665
5520
|
};
|
|
4666
5521
|
|
|
4667
5522
|
// src/server/routes/interactions.ts
|
|
4668
|
-
import { z } from "zod";
|
|
5523
|
+
import { z as z3 } from "zod";
|
|
4669
5524
|
|
|
4670
5525
|
// src/server/utils.ts
|
|
4671
5526
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
@@ -4688,8 +5543,8 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
4688
5543
|
}
|
|
4689
5544
|
|
|
4690
5545
|
// src/server/routes/interactions.ts
|
|
4691
|
-
var InteractionUpdateSchema =
|
|
4692
|
-
status:
|
|
5546
|
+
var InteractionUpdateSchema = z3.object({
|
|
5547
|
+
status: z3.enum(["pending", "claimed", "resolved"])
|
|
4693
5548
|
});
|
|
4694
5549
|
var SAFE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
4695
5550
|
function sendJson(res, status, body) {
|
|
@@ -4740,12 +5595,12 @@ function handleInteractionsRoute(req, res, queue) {
|
|
|
4740
5595
|
}
|
|
4741
5596
|
|
|
4742
5597
|
// src/server/routes/plans.ts
|
|
4743
|
-
import { z as
|
|
5598
|
+
import { z as z4 } from "zod";
|
|
4744
5599
|
import * as fs9 from "fs/promises";
|
|
4745
5600
|
import * as path9 from "path";
|
|
4746
|
-
var PlanWriteSchema =
|
|
4747
|
-
filename:
|
|
4748
|
-
content:
|
|
5601
|
+
var PlanWriteSchema = z4.object({
|
|
5602
|
+
filename: z4.string().min(1),
|
|
5603
|
+
content: z4.string().min(1)
|
|
4749
5604
|
});
|
|
4750
5605
|
function handlePlansRoute(req, res, plansDir) {
|
|
4751
5606
|
const { method, url } = req;
|
|
@@ -4789,7 +5644,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
4789
5644
|
import { spawn as spawn4 } from "child_process";
|
|
4790
5645
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
4791
5646
|
import * as readline2 from "readline";
|
|
4792
|
-
import { z as
|
|
5647
|
+
import { z as z5 } from "zod";
|
|
4793
5648
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4794
5649
|
var SAFE_ENV_PREFIXES = [
|
|
4795
5650
|
"PATH",
|
|
@@ -4824,10 +5679,10 @@ function buildChildEnv() {
|
|
|
4824
5679
|
}
|
|
4825
5680
|
return env;
|
|
4826
5681
|
}
|
|
4827
|
-
var ChatRequestSchema =
|
|
4828
|
-
prompt:
|
|
4829
|
-
system:
|
|
4830
|
-
sessionId:
|
|
5682
|
+
var ChatRequestSchema = z5.object({
|
|
5683
|
+
prompt: z5.string().min(1),
|
|
5684
|
+
system: z5.string().optional(),
|
|
5685
|
+
sessionId: z5.string().regex(UUID_RE).optional()
|
|
4831
5686
|
});
|
|
4832
5687
|
function handleChatProxyRoute(req, res, command = "claude") {
|
|
4833
5688
|
const { method, url } = req;
|
|
@@ -5037,11 +5892,11 @@ function extractChunks(event) {
|
|
|
5037
5892
|
|
|
5038
5893
|
// src/server/routes/analyze.ts
|
|
5039
5894
|
import { manualToRawWorkItem, scoreToConcernSignals } from "@harness-engineering/intelligence";
|
|
5040
|
-
import { z as
|
|
5041
|
-
var AnalyzeRequestSchema =
|
|
5042
|
-
title:
|
|
5043
|
-
description:
|
|
5044
|
-
labels:
|
|
5895
|
+
import { z as z6 } from "zod";
|
|
5896
|
+
var AnalyzeRequestSchema = z6.object({
|
|
5897
|
+
title: z6.string().min(1),
|
|
5898
|
+
description: z6.string().optional(),
|
|
5899
|
+
labels: z6.array(z6.string()).optional()
|
|
5045
5900
|
});
|
|
5046
5901
|
function emit2(res, event) {
|
|
5047
5902
|
res.write(`data: ${JSON.stringify(event)}
|
|
@@ -5149,19 +6004,19 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
5149
6004
|
// src/server/routes/roadmap-actions.ts
|
|
5150
6005
|
import * as fs10 from "fs/promises";
|
|
5151
6006
|
import { parseRoadmap as parseRoadmap2, serializeRoadmap as serializeRoadmap2 } from "@harness-engineering/core";
|
|
5152
|
-
import { z as
|
|
5153
|
-
var AppendRoadmapRequestSchema =
|
|
5154
|
-
title:
|
|
5155
|
-
summary:
|
|
5156
|
-
labels:
|
|
5157
|
-
enrichedSpec:
|
|
5158
|
-
intent:
|
|
5159
|
-
unknowns:
|
|
5160
|
-
ambiguities:
|
|
5161
|
-
riskSignals:
|
|
5162
|
-
affectedSystems:
|
|
6007
|
+
import { z as z7 } from "zod";
|
|
6008
|
+
var AppendRoadmapRequestSchema = z7.object({
|
|
6009
|
+
title: z7.string().min(1),
|
|
6010
|
+
summary: z7.string().optional(),
|
|
6011
|
+
labels: z7.array(z7.string()).optional(),
|
|
6012
|
+
enrichedSpec: z7.object({
|
|
6013
|
+
intent: z7.string(),
|
|
6014
|
+
unknowns: z7.array(z7.string()),
|
|
6015
|
+
ambiguities: z7.array(z7.string()),
|
|
6016
|
+
riskSignals: z7.array(z7.string()),
|
|
6017
|
+
affectedSystems: z7.array(z7.object({ name: z7.string() }))
|
|
5163
6018
|
}).optional(),
|
|
5164
|
-
cmlRecommendedRoute:
|
|
6019
|
+
cmlRecommendedRoute: z7.enum(["local", "human", "simulation-required"]).optional()
|
|
5165
6020
|
});
|
|
5166
6021
|
function sendJSON(res, status, body) {
|
|
5167
6022
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -5232,11 +6087,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
5232
6087
|
|
|
5233
6088
|
// src/server/routes/dispatch-actions.ts
|
|
5234
6089
|
import { createHash as createHash3 } from "crypto";
|
|
5235
|
-
import { z as
|
|
5236
|
-
var DispatchAdHocRequestSchema =
|
|
5237
|
-
title:
|
|
5238
|
-
description:
|
|
5239
|
-
labels:
|
|
6090
|
+
import { z as z8 } from "zod";
|
|
6091
|
+
var DispatchAdHocRequestSchema = z8.object({
|
|
6092
|
+
title: z8.string().min(1),
|
|
6093
|
+
description: z8.string().optional(),
|
|
6094
|
+
labels: z8.array(z8.string()).optional()
|
|
5240
6095
|
});
|
|
5241
6096
|
function sendJSON2(res, status, body) {
|
|
5242
6097
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -5333,9 +6188,9 @@ function handleAnalysesRoute(req, res, archive) {
|
|
|
5333
6188
|
}
|
|
5334
6189
|
|
|
5335
6190
|
// src/server/routes/maintenance.ts
|
|
5336
|
-
import { z as
|
|
5337
|
-
var TriggerRequestSchema =
|
|
5338
|
-
taskId:
|
|
6191
|
+
import { z as z9 } from "zod";
|
|
6192
|
+
var TriggerRequestSchema = z9.object({
|
|
6193
|
+
taskId: z9.string().min(1)
|
|
5339
6194
|
});
|
|
5340
6195
|
function sendJSON3(res, status, body) {
|
|
5341
6196
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
@@ -5414,9 +6269,9 @@ function handleMaintenanceRoute(req, res, deps) {
|
|
|
5414
6269
|
// src/server/routes/sessions.ts
|
|
5415
6270
|
import * as fs11 from "fs/promises";
|
|
5416
6271
|
import * as path10 from "path";
|
|
5417
|
-
import { z as
|
|
5418
|
-
var SessionCreateSchema =
|
|
5419
|
-
sessionId:
|
|
6272
|
+
import { z as z10 } from "zod";
|
|
6273
|
+
var SessionCreateSchema = z10.object({
|
|
6274
|
+
sessionId: z10.string().min(1)
|
|
5420
6275
|
}).passthrough();
|
|
5421
6276
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5422
6277
|
function isSafeId(id) {
|
|
@@ -5503,7 +6358,7 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
5503
6358
|
return;
|
|
5504
6359
|
}
|
|
5505
6360
|
const body = await readBody(req);
|
|
5506
|
-
const updates =
|
|
6361
|
+
const updates = z10.record(z10.unknown()).parse(JSON.parse(body));
|
|
5507
6362
|
const sessionFilePath = path10.join(sessionsDir, id, "session.json");
|
|
5508
6363
|
const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
|
|
5509
6364
|
await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
@@ -5622,6 +6477,42 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
5622
6477
|
return true;
|
|
5623
6478
|
}
|
|
5624
6479
|
|
|
6480
|
+
// src/server/routes/local-model.ts
|
|
6481
|
+
function sendJSON4(res, status, body) {
|
|
6482
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6483
|
+
res.end(JSON.stringify(body));
|
|
6484
|
+
}
|
|
6485
|
+
function handleLocalModelRoute(req, res, getStatus) {
|
|
6486
|
+
const { method, url } = req;
|
|
6487
|
+
if (url !== "/api/v1/local-model/status") return false;
|
|
6488
|
+
if (method !== "GET") {
|
|
6489
|
+
sendJSON4(res, 405, { error: "Method not allowed" });
|
|
6490
|
+
return true;
|
|
6491
|
+
}
|
|
6492
|
+
if (!getStatus) {
|
|
6493
|
+
sendJSON4(res, 503, { error: "Local backend not configured" });
|
|
6494
|
+
return true;
|
|
6495
|
+
}
|
|
6496
|
+
const status = getStatus();
|
|
6497
|
+
if (!status) {
|
|
6498
|
+
sendJSON4(res, 503, { error: "Local backend not configured" });
|
|
6499
|
+
return true;
|
|
6500
|
+
}
|
|
6501
|
+
sendJSON4(res, 200, status);
|
|
6502
|
+
return true;
|
|
6503
|
+
}
|
|
6504
|
+
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
6505
|
+
const { method, url } = req;
|
|
6506
|
+
if (url !== "/api/v1/local-models/status") return false;
|
|
6507
|
+
if (method !== "GET") {
|
|
6508
|
+
sendJSON4(res, 405, { error: "Method not allowed" });
|
|
6509
|
+
return true;
|
|
6510
|
+
}
|
|
6511
|
+
const statuses = getStatuses ? getStatuses() : [];
|
|
6512
|
+
sendJSON4(res, 200, statuses);
|
|
6513
|
+
return true;
|
|
6514
|
+
}
|
|
6515
|
+
|
|
5625
6516
|
// src/server/static.ts
|
|
5626
6517
|
import * as fs12 from "fs";
|
|
5627
6518
|
import * as path11 from "path";
|
|
@@ -5775,6 +6666,8 @@ var OrchestratorServer = class {
|
|
|
5775
6666
|
dispatchAdHoc;
|
|
5776
6667
|
sessionsDir;
|
|
5777
6668
|
maintenanceDeps = null;
|
|
6669
|
+
getLocalModelStatus = null;
|
|
6670
|
+
getLocalModelStatuses = null;
|
|
5778
6671
|
recorder = null;
|
|
5779
6672
|
planWatcher = null;
|
|
5780
6673
|
stateChangeListener;
|
|
@@ -5801,6 +6694,8 @@ var OrchestratorServer = class {
|
|
|
5801
6694
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
5802
6695
|
this.sessionsDir = deps?.sessionsDir ?? path13.resolve(".harness", "sessions");
|
|
5803
6696
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
6697
|
+
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
6698
|
+
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
5804
6699
|
}
|
|
5805
6700
|
wireEvents() {
|
|
5806
6701
|
this.stateChangeListener = (snapshot) => {
|
|
@@ -5827,6 +6722,31 @@ var OrchestratorServer = class {
|
|
|
5827
6722
|
broadcastMaintenance(type, data) {
|
|
5828
6723
|
this.broadcaster.broadcast(type, data);
|
|
5829
6724
|
}
|
|
6725
|
+
/**
|
|
6726
|
+
* Broadcast a local-model status change to dashboard clients.
|
|
6727
|
+
*
|
|
6728
|
+
* Phase 3 routes status events through the existing WebSocket broadcaster
|
|
6729
|
+
* on topic `local-model:status` so test fixtures and dashboard consumers
|
|
6730
|
+
* observe payloads immediately. The project broadcasts via WebSocket; the
|
|
6731
|
+
* spec's "SSE topic" wording is approximate. Phase 5 widens the payload
|
|
6732
|
+
* to `NamedLocalModelStatus` (with `backendName` + `endpoint`); the channel
|
|
6733
|
+
* and bind-before-probe ordering are unchanged.
|
|
6734
|
+
*/
|
|
6735
|
+
broadcastLocalModelStatus(status) {
|
|
6736
|
+
this.broadcaster.broadcast("local-model:status", status);
|
|
6737
|
+
}
|
|
6738
|
+
/**
|
|
6739
|
+
* Update the intelligence pipeline reference after construction.
|
|
6740
|
+
*
|
|
6741
|
+
* The orchestrator constructs the pipeline lazily inside `start()` (the
|
|
6742
|
+
* resolver must observe the server before pipeline construction). The
|
|
6743
|
+
* server is built in the orchestrator constructor with `pipeline: null`,
|
|
6744
|
+
* so it must be told the real pipeline once it's been created — otherwise
|
|
6745
|
+
* `/api/analyze` would always see a null pipeline and return 503.
|
|
6746
|
+
*/
|
|
6747
|
+
setPipeline(pipeline) {
|
|
6748
|
+
this.pipeline = pipeline;
|
|
6749
|
+
}
|
|
5830
6750
|
/**
|
|
5831
6751
|
* Set (or update) the maintenance route dependencies after construction.
|
|
5832
6752
|
* Called by the Orchestrator once the scheduler and reporter are ready.
|
|
@@ -5903,6 +6823,12 @@ var OrchestratorServer = class {
|
|
|
5903
6823
|
if (handleDispatchActionsRoute(req, res, this.dispatchAdHoc)) {
|
|
5904
6824
|
return true;
|
|
5905
6825
|
}
|
|
6826
|
+
if (handleLocalModelRoute(req, res, this.getLocalModelStatus)) {
|
|
6827
|
+
return true;
|
|
6828
|
+
}
|
|
6829
|
+
if (handleLocalModelsRoute(req, res, this.getLocalModelStatuses)) {
|
|
6830
|
+
return true;
|
|
6831
|
+
}
|
|
5906
6832
|
if (handleMaintenanceRoute(req, res, this.maintenanceDeps)) {
|
|
5907
6833
|
return true;
|
|
5908
6834
|
}
|
|
@@ -6475,17 +7401,17 @@ var MaintenanceScheduler = class {
|
|
|
6475
7401
|
// src/maintenance/reporter.ts
|
|
6476
7402
|
import * as fs14 from "fs";
|
|
6477
7403
|
import * as path14 from "path";
|
|
6478
|
-
import { z as
|
|
6479
|
-
var RunResultSchema =
|
|
6480
|
-
taskId:
|
|
6481
|
-
startedAt:
|
|
6482
|
-
completedAt:
|
|
6483
|
-
status:
|
|
6484
|
-
findings:
|
|
6485
|
-
fixed:
|
|
6486
|
-
prUrl:
|
|
6487
|
-
prUpdated:
|
|
6488
|
-
error:
|
|
7404
|
+
import { z as z11 } from "zod";
|
|
7405
|
+
var RunResultSchema = z11.object({
|
|
7406
|
+
taskId: z11.string(),
|
|
7407
|
+
startedAt: z11.string(),
|
|
7408
|
+
completedAt: z11.string(),
|
|
7409
|
+
status: z11.enum(["success", "failure", "skipped", "no-issues"]),
|
|
7410
|
+
findings: z11.number(),
|
|
7411
|
+
fixed: z11.number(),
|
|
7412
|
+
prUrl: z11.string().nullable(),
|
|
7413
|
+
prUpdated: z11.boolean(),
|
|
7414
|
+
error: z11.string().optional()
|
|
6489
7415
|
});
|
|
6490
7416
|
var MAX_HISTORY = 500;
|
|
6491
7417
|
var fallbackLogger = {
|
|
@@ -6512,7 +7438,7 @@ var MaintenanceReporter = class {
|
|
|
6512
7438
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
6513
7439
|
const filePath = path14.join(this.persistDir, "history.json");
|
|
6514
7440
|
const data = await fs14.promises.readFile(filePath, "utf-8");
|
|
6515
|
-
const parsed =
|
|
7441
|
+
const parsed = z11.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
6516
7442
|
if (parsed.success) {
|
|
6517
7443
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
6518
7444
|
}
|
|
@@ -6795,13 +7721,43 @@ var TaskRunner = class {
|
|
|
6795
7721
|
};
|
|
6796
7722
|
|
|
6797
7723
|
// src/orchestrator.ts
|
|
7724
|
+
function useCaseForBackendParam(issue, backendParam) {
|
|
7725
|
+
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
7726
|
+
const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
7727
|
+
return { kind: "tier", tier };
|
|
7728
|
+
}
|
|
6798
7729
|
var Orchestrator = class extends EventEmitter {
|
|
6799
7730
|
state;
|
|
6800
7731
|
config;
|
|
6801
7732
|
tracker;
|
|
6802
7733
|
workspace;
|
|
6803
7734
|
hooks;
|
|
6804
|
-
|
|
7735
|
+
/**
|
|
7736
|
+
* Spec 2 SC30 / Task 11: per-dispatch backend factory replaces the
|
|
7737
|
+
* Phase 1 `runner` / `localRunner` two-runner split. Each
|
|
7738
|
+
* `dispatchIssue()` call asks the factory for a `RoutingUseCase`-routed
|
|
7739
|
+
* `AgentBackend`, then wraps it in a fresh `AgentRunner`.
|
|
7740
|
+
*
|
|
7741
|
+
* `AgentRunner` is stateless (just `{ backend, options }`), so
|
|
7742
|
+
* per-dispatch construction is safe and avoids the cross-call state
|
|
7743
|
+
* the old two-runner split had to coordinate.
|
|
7744
|
+
*
|
|
7745
|
+
* Null only in the legacy fallback path: when `migrateAgentConfig`
|
|
7746
|
+
* throws (legacy configs missing supplemental fields, e.g.
|
|
7747
|
+
* `agent.backend='anthropic'` with no `agent.model`) AND no
|
|
7748
|
+
* `overrides.backend` is supplied, factory construction is skipped to
|
|
7749
|
+
* preserve the prior behavior of failing at dispatch time rather than
|
|
7750
|
+
* construction time. Eliminating this fallback is autopilot Phase 4+.
|
|
7751
|
+
*/
|
|
7752
|
+
backendFactory;
|
|
7753
|
+
/**
|
|
7754
|
+
* Test-only: when overrides.backend is provided, dispatch uses this
|
|
7755
|
+
* instance directly (bypassing the factory). Mirrors Phase 1
|
|
7756
|
+
* `overrides.backend → this.runner.backend` behavior so existing
|
|
7757
|
+
* MockBackend-injection tests keep working without touching the
|
|
7758
|
+
* factory's routing path.
|
|
7759
|
+
*/
|
|
7760
|
+
overrideBackend;
|
|
6805
7761
|
renderer;
|
|
6806
7762
|
promptTemplate;
|
|
6807
7763
|
server;
|
|
@@ -6809,7 +7765,22 @@ var Orchestrator = class extends EventEmitter {
|
|
|
6809
7765
|
heartbeatInterval;
|
|
6810
7766
|
logger;
|
|
6811
7767
|
interactionQueue;
|
|
6812
|
-
|
|
7768
|
+
/**
|
|
7769
|
+
* Per-named-backend resolver map (Spec 2 SC37). Each `local`/`pi` entry
|
|
7770
|
+
* in `agent.backends` spawns one `LocalModelResolver`. Legacy
|
|
7771
|
+
* single-backend configs converge here via `migrateAgentConfig` (Task 9),
|
|
7772
|
+
* so this map is the single source of truth post-migration.
|
|
7773
|
+
*/
|
|
7774
|
+
localResolvers = /* @__PURE__ */ new Map();
|
|
7775
|
+
/**
|
|
7776
|
+
* Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
|
|
7777
|
+
* (SC39): each local/pi resolver gets its own listener emitting a
|
|
7778
|
+
* `NamedLocalModelStatus` event tagged with `backendName` + `endpoint`.
|
|
7779
|
+
* The previous single-resolver field (`localModelStatusUnsubscribe`)
|
|
7780
|
+
* is replaced by this list so multi-local configs can teardown all
|
|
7781
|
+
* listeners on `stop()` without a Map mutation.
|
|
7782
|
+
*/
|
|
7783
|
+
localModelStatusUnsubscribes = [];
|
|
6813
7784
|
pipeline;
|
|
6814
7785
|
analysisArchive;
|
|
6815
7786
|
graphStore = null;
|
|
@@ -6851,20 +7822,60 @@ var Orchestrator = class extends EventEmitter {
|
|
|
6851
7822
|
this.promptTemplate = promptTemplate;
|
|
6852
7823
|
this.state = createEmptyState(config);
|
|
6853
7824
|
this.logger = new StructuredLogger();
|
|
7825
|
+
try {
|
|
7826
|
+
const migrationResult = migrateAgentConfig(this.config.agent);
|
|
7827
|
+
if (migrationResult.warnings.length > 0) {
|
|
7828
|
+
for (const w of migrationResult.warnings) this.logger.warn(w);
|
|
7829
|
+
}
|
|
7830
|
+
this.config = { ...this.config, agent: migrationResult.config };
|
|
7831
|
+
} catch (err) {
|
|
7832
|
+
this.logger.warn(
|
|
7833
|
+
`migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
7834
|
+
);
|
|
7835
|
+
}
|
|
6854
7836
|
this.tracker = overrides?.tracker || this.createTracker();
|
|
6855
7837
|
this.workspace = new WorkspaceManager(config.workspace);
|
|
6856
7838
|
this.hooks = new WorkspaceHooks(config.hooks);
|
|
6857
7839
|
this.renderer = new PromptRenderer();
|
|
6858
|
-
this.
|
|
6859
|
-
maxTurns: config.agent.maxTurns
|
|
6860
|
-
});
|
|
7840
|
+
this.overrideBackend = overrides?.backend ?? null;
|
|
6861
7841
|
this.interactionQueue = new InteractionQueue(
|
|
6862
7842
|
path15.join(config.workspace.root, "..", "interactions")
|
|
6863
7843
|
);
|
|
6864
7844
|
this.analysisArchive = new AnalysisArchive(path15.join(config.workspace.root, "..", "analyses"));
|
|
6865
|
-
const
|
|
6866
|
-
|
|
6867
|
-
|
|
7845
|
+
const backendsMap = this.config.agent.backends ?? {};
|
|
7846
|
+
for (const [name, def] of Object.entries(backendsMap)) {
|
|
7847
|
+
if (def.type === "local" || def.type === "pi") {
|
|
7848
|
+
const resolverOpts = {
|
|
7849
|
+
endpoint: def.endpoint,
|
|
7850
|
+
configured: typeof def.model === "string" ? [def.model] : def.model,
|
|
7851
|
+
logger: this.logger
|
|
7852
|
+
};
|
|
7853
|
+
if (def.apiKey !== void 0) resolverOpts.apiKey = def.apiKey;
|
|
7854
|
+
if (def.probeIntervalMs !== void 0) resolverOpts.probeIntervalMs = def.probeIntervalMs;
|
|
7855
|
+
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
7856
|
+
}
|
|
7857
|
+
}
|
|
7858
|
+
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
7859
|
+
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
7860
|
+
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
7861
|
+
const routing = this.config.agent.routing ?? {
|
|
7862
|
+
default: firstBackendName ?? "primary"
|
|
7863
|
+
};
|
|
7864
|
+
this.backendFactory = new OrchestratorBackendFactory({
|
|
7865
|
+
backends: this.config.agent.backends,
|
|
7866
|
+
routing,
|
|
7867
|
+
sandboxPolicy,
|
|
7868
|
+
...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
|
|
7869
|
+
...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
|
|
7870
|
+
getResolverModelFor: (name) => {
|
|
7871
|
+
const resolver = this.localResolvers.get(name);
|
|
7872
|
+
return resolver ? () => resolver.resolveModel() : void 0;
|
|
7873
|
+
}
|
|
7874
|
+
});
|
|
7875
|
+
} else {
|
|
7876
|
+
this.backendFactory = null;
|
|
7877
|
+
}
|
|
7878
|
+
this.pipeline = null;
|
|
6868
7879
|
this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
|
|
6869
7880
|
this.prDetector = new PRDetector({
|
|
6870
7881
|
logger: this.logger,
|
|
@@ -6908,7 +7919,25 @@ var Orchestrator = class extends EventEmitter {
|
|
|
6908
7919
|
pipeline: this.pipeline,
|
|
6909
7920
|
analysisArchive: this.analysisArchive,
|
|
6910
7921
|
roadmapPath: config.tracker.filePath ?? null,
|
|
6911
|
-
dispatchAdHoc: this.dispatchAdHoc.bind(this)
|
|
7922
|
+
dispatchAdHoc: this.dispatchAdHoc.bind(this),
|
|
7923
|
+
getLocalModelStatus: () => {
|
|
7924
|
+
const first = this.localResolvers.values().next();
|
|
7925
|
+
return first.done ? null : first.value.getStatus();
|
|
7926
|
+
},
|
|
7927
|
+
getLocalModelStatuses: () => {
|
|
7928
|
+
const backends = this.config.agent.backends ?? {};
|
|
7929
|
+
const out = [];
|
|
7930
|
+
for (const [name, resolver] of this.localResolvers) {
|
|
7931
|
+
const def = backends[name];
|
|
7932
|
+
if (!def || def.type !== "local" && def.type !== "pi") continue;
|
|
7933
|
+
out.push({
|
|
7934
|
+
...resolver.getStatus(),
|
|
7935
|
+
backendName: name,
|
|
7936
|
+
endpoint: def.endpoint
|
|
7937
|
+
});
|
|
7938
|
+
}
|
|
7939
|
+
return out;
|
|
7940
|
+
}
|
|
6912
7941
|
});
|
|
6913
7942
|
this.server.setRecorder(this.recorder);
|
|
6914
7943
|
this.interactionQueue.onPush((interaction) => {
|
|
@@ -6922,44 +7951,6 @@ var Orchestrator = class extends EventEmitter {
|
|
|
6922
7951
|
}
|
|
6923
7952
|
throw new Error(`Unsupported tracker kind: ${this.config.tracker.kind}`);
|
|
6924
7953
|
}
|
|
6925
|
-
createBackend() {
|
|
6926
|
-
let backend;
|
|
6927
|
-
if (this.config.agent.backend === "mock") {
|
|
6928
|
-
backend = new MockBackend();
|
|
6929
|
-
} else if (this.config.agent.backend === "claude") {
|
|
6930
|
-
backend = new ClaudeBackend(this.config.agent.command);
|
|
6931
|
-
} else if (this.config.agent.backend === "openai") {
|
|
6932
|
-
backend = new OpenAIBackend({
|
|
6933
|
-
...this.config.agent.model !== void 0 && { model: this.config.agent.model },
|
|
6934
|
-
...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
|
|
6935
|
-
});
|
|
6936
|
-
} else if (this.config.agent.backend === "gemini") {
|
|
6937
|
-
backend = new GeminiBackend({
|
|
6938
|
-
...this.config.agent.model !== void 0 && { model: this.config.agent.model },
|
|
6939
|
-
...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
|
|
6940
|
-
});
|
|
6941
|
-
} else if (this.config.agent.backend === "anthropic") {
|
|
6942
|
-
backend = new AnthropicBackend({
|
|
6943
|
-
...this.config.agent.model !== void 0 && { model: this.config.agent.model },
|
|
6944
|
-
...this.config.agent.apiKey !== void 0 && { apiKey: this.config.agent.apiKey }
|
|
6945
|
-
});
|
|
6946
|
-
} else {
|
|
6947
|
-
throw new Error(`Unsupported agent backend: ${this.config.agent.backend}`);
|
|
6948
|
-
}
|
|
6949
|
-
if (this.config.agent.sandboxPolicy === "docker" && this.config.agent.container) {
|
|
6950
|
-
const runtime = new DockerRuntime();
|
|
6951
|
-
const secretBackend = this.config.agent.secrets ? createSecretBackend(this.config.agent.secrets) : null;
|
|
6952
|
-
const secretKeys = this.config.agent.secrets?.keys ?? [];
|
|
6953
|
-
backend = new ContainerBackend(
|
|
6954
|
-
backend,
|
|
6955
|
-
runtime,
|
|
6956
|
-
secretBackend,
|
|
6957
|
-
this.config.agent.container,
|
|
6958
|
-
secretKeys
|
|
6959
|
-
);
|
|
6960
|
-
}
|
|
6961
|
-
return backend;
|
|
6962
|
-
}
|
|
6963
7954
|
/**
|
|
6964
7955
|
* Creates a TaskRunner for the maintenance scheduler.
|
|
6965
7956
|
* CheckCommandRunner and CommandExecutor use real child_process execution.
|
|
@@ -7085,97 +8076,98 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7085
8076
|
});
|
|
7086
8077
|
}
|
|
7087
8078
|
}
|
|
7088
|
-
createLocalBackend() {
|
|
7089
|
-
if (this.config.agent.localBackend === "openai-compatible") {
|
|
7090
|
-
const localConfig = {};
|
|
7091
|
-
if (this.config.agent.localEndpoint) localConfig.endpoint = this.config.agent.localEndpoint;
|
|
7092
|
-
if (this.config.agent.localModel) localConfig.model = this.config.agent.localModel;
|
|
7093
|
-
if (this.config.agent.localApiKey) localConfig.apiKey = this.config.agent.localApiKey;
|
|
7094
|
-
if (this.config.agent.localTimeoutMs)
|
|
7095
|
-
localConfig.timeoutMs = this.config.agent.localTimeoutMs;
|
|
7096
|
-
return new LocalBackend(localConfig);
|
|
7097
|
-
}
|
|
7098
|
-
if (this.config.agent.localBackend === "pi") {
|
|
7099
|
-
return new PiBackend({
|
|
7100
|
-
model: this.config.agent.localModel,
|
|
7101
|
-
endpoint: this.config.agent.localEndpoint,
|
|
7102
|
-
apiKey: this.config.agent.localApiKey
|
|
7103
|
-
});
|
|
7104
|
-
}
|
|
7105
|
-
return null;
|
|
7106
|
-
}
|
|
7107
8079
|
createIntelligencePipeline() {
|
|
7108
8080
|
const intel = this.config.intelligence;
|
|
7109
8081
|
if (!intel?.enabled) return null;
|
|
7110
|
-
const
|
|
7111
|
-
if (!
|
|
8082
|
+
const selProvider = this.createAnalysisProvider("sel");
|
|
8083
|
+
if (!selProvider) return null;
|
|
8084
|
+
const routing = this.config.agent.routing;
|
|
8085
|
+
const peslName = routing?.intelligence?.pesl;
|
|
8086
|
+
const selName = routing?.intelligence?.sel ?? routing?.default;
|
|
8087
|
+
const peslProvider = peslName !== void 0 && peslName !== selName ? this.createAnalysisProvider("pesl") : null;
|
|
7112
8088
|
const peslModel = intel.models?.pesl ?? this.config.agent.model;
|
|
7113
8089
|
const store = new GraphStore();
|
|
7114
8090
|
this.graphStore = store;
|
|
7115
|
-
return new IntelligencePipeline(
|
|
7116
|
-
...peslModel !== void 0 && { peslModel }
|
|
8091
|
+
return new IntelligencePipeline(selProvider, store, {
|
|
8092
|
+
...peslModel !== void 0 && { peslModel },
|
|
8093
|
+
...peslProvider !== null && peslProvider !== void 0 && { peslProvider }
|
|
7117
8094
|
});
|
|
7118
8095
|
}
|
|
7119
8096
|
/**
|
|
7120
|
-
* Create the AnalysisProvider for
|
|
8097
|
+
* Create the AnalysisProvider for an intelligence-pipeline layer
|
|
8098
|
+
* (`sel` by default; `pesl` when constructing a distinct PESL
|
|
8099
|
+
* provider per Spec 2 SC35).
|
|
8100
|
+
*
|
|
8101
|
+
* Spec 2 Phase 4 (SC31–SC36) — resolution order:
|
|
8102
|
+
* 1. Explicit `intelligence.provider` config wins (preserves Phase 0–3 behavior; SC33).
|
|
8103
|
+
* 2. Otherwise, consult `agent.routing.intelligence.<layer>` (or
|
|
8104
|
+
* `routing.default`) to pick a `BackendDef` from `agent.backends`,
|
|
8105
|
+
* then translate via `buildAnalysisProvider` (the per-type factory).
|
|
7121
8106
|
*
|
|
7122
|
-
*
|
|
7123
|
-
*
|
|
7124
|
-
*
|
|
7125
|
-
*
|
|
8107
|
+
* Closes the Phase 2 deferral (P2-DEF-638): the legacy
|
|
8108
|
+
* `this.config.agent.backend` read at the bottom of this method is
|
|
8109
|
+
* removed; routing is the sole source for non-explicit configs.
|
|
8110
|
+
*
|
|
8111
|
+
* Cyclomatic complexity: pre-Phase-4 was 33 (factory dispatch was
|
|
8112
|
+
* inlined here). Phase 4 extracts the per-type tree into
|
|
8113
|
+
* `buildAnalysisProvider`, dropping this method to ≤ 5 branches
|
|
8114
|
+
* (under the 15 threshold).
|
|
7126
8115
|
*/
|
|
7127
|
-
createAnalysisProvider() {
|
|
8116
|
+
createAnalysisProvider(layer = "sel") {
|
|
7128
8117
|
const intel = this.config.intelligence;
|
|
7129
|
-
|
|
7130
|
-
if (intel
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
const model = selModel ?? this.config.agent.localModel;
|
|
7137
|
-
this.logger.info(`Intelligence pipeline using local backend at ${endpoint}`);
|
|
7138
|
-
return new OpenAICompatibleAnalysisProvider({
|
|
7139
|
-
apiKey,
|
|
7140
|
-
baseUrl: endpoint,
|
|
7141
|
-
...model !== void 0 && { defaultModel: model },
|
|
7142
|
-
...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs },
|
|
7143
|
-
...intel?.promptSuffix !== void 0 && { promptSuffix: intel.promptSuffix },
|
|
7144
|
-
...intel?.jsonMode !== void 0 && { jsonMode: intel.jsonMode }
|
|
7145
|
-
});
|
|
7146
|
-
}
|
|
7147
|
-
const backend = this.config.agent.backend;
|
|
7148
|
-
if (backend === "anthropic" || backend === "claude") {
|
|
7149
|
-
const apiKey = this.config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
7150
|
-
if (apiKey) {
|
|
7151
|
-
return new AnthropicAnalysisProvider({
|
|
7152
|
-
apiKey,
|
|
7153
|
-
...selModel !== void 0 && { defaultModel: selModel }
|
|
7154
|
-
});
|
|
7155
|
-
}
|
|
7156
|
-
}
|
|
7157
|
-
if (backend === "openai") {
|
|
7158
|
-
const apiKey = this.config.agent.apiKey ?? process.env.OPENAI_API_KEY;
|
|
7159
|
-
if (apiKey) {
|
|
7160
|
-
return new OpenAICompatibleAnalysisProvider({
|
|
7161
|
-
apiKey,
|
|
7162
|
-
baseUrl: "https://api.openai.com/v1",
|
|
7163
|
-
...selModel !== void 0 && { defaultModel: selModel }
|
|
7164
|
-
});
|
|
7165
|
-
}
|
|
8118
|
+
if (!intel?.enabled) return null;
|
|
8119
|
+
if (intel.provider) {
|
|
8120
|
+
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
8121
|
+
return this.createProviderFromExplicitConfig(
|
|
8122
|
+
intel.provider,
|
|
8123
|
+
layerModel ?? this.config.agent.model
|
|
8124
|
+
);
|
|
7166
8125
|
}
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
8126
|
+
const routed = this.resolveRoutedBackendForIntelligence(layer);
|
|
8127
|
+
if (!routed) return null;
|
|
8128
|
+
const { name, def } = routed;
|
|
8129
|
+
const resolver = this.localResolvers.get(name);
|
|
8130
|
+
return buildAnalysisProvider({
|
|
8131
|
+
def,
|
|
8132
|
+
backendName: name,
|
|
8133
|
+
layer,
|
|
8134
|
+
// Spec 2 P3-IMP-1: a single snapshot read feeds the factory's
|
|
8135
|
+
// unavailable-warn diagnostic (Configured/Detected lists) and
|
|
8136
|
+
// collapses the two `getStatus()` calls flagged by P3-SUG-2.
|
|
8137
|
+
getResolverStatusSnapshot: () => {
|
|
8138
|
+
if (!resolver) return null;
|
|
8139
|
+
const status = resolver.getStatus();
|
|
8140
|
+
return {
|
|
8141
|
+
available: status.available,
|
|
8142
|
+
resolved: status.resolved,
|
|
8143
|
+
configured: status.configured,
|
|
8144
|
+
detected: status.detected
|
|
8145
|
+
};
|
|
8146
|
+
},
|
|
8147
|
+
intelligence: intel,
|
|
8148
|
+
logger: this.logger
|
|
8149
|
+
});
|
|
8150
|
+
}
|
|
8151
|
+
/**
|
|
8152
|
+
* Look up the routed BackendDef for an intelligence layer, falling
|
|
8153
|
+
* back through `routing.intelligence.<layer>` → `routing.default`
|
|
8154
|
+
* → null. Returns the resolved name alongside the def so callers can
|
|
8155
|
+
* key into the per-name resolver map.
|
|
8156
|
+
*/
|
|
8157
|
+
resolveRoutedBackendForIntelligence(layer) {
|
|
8158
|
+
const routing = this.config.agent.routing;
|
|
8159
|
+
const backends = this.config.agent.backends;
|
|
8160
|
+
if (!routing || !backends) return null;
|
|
8161
|
+
const layerName = routing.intelligence?.[layer];
|
|
8162
|
+
const name = layerName ?? routing.default;
|
|
8163
|
+
const def = backends[name];
|
|
8164
|
+
if (!def) {
|
|
8165
|
+
this.logger.warn(
|
|
8166
|
+
`Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
|
|
8167
|
+
);
|
|
8168
|
+
return null;
|
|
7174
8169
|
}
|
|
7175
|
-
|
|
7176
|
-
`Intelligence pipeline: unsupported backend "${backend}". Supported: anthropic, claude, openai, or localBackend: openai-compatible / pi.`
|
|
7177
|
-
);
|
|
7178
|
-
return null;
|
|
8170
|
+
return { name, def };
|
|
7179
8171
|
}
|
|
7180
8172
|
createProviderFromExplicitConfig(provider, selModel) {
|
|
7181
8173
|
if (provider.kind === "anthropic") {
|
|
@@ -7183,13 +8175,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7183
8175
|
if (!apiKey2) {
|
|
7184
8176
|
throw new Error("Intelligence pipeline: no Anthropic API key found.");
|
|
7185
8177
|
}
|
|
7186
|
-
return new
|
|
8178
|
+
return new AnthropicAnalysisProvider2({
|
|
7187
8179
|
apiKey: apiKey2,
|
|
7188
8180
|
...selModel !== void 0 && { defaultModel: selModel }
|
|
7189
8181
|
});
|
|
7190
8182
|
}
|
|
7191
8183
|
if (provider.kind === "claude-cli") {
|
|
7192
|
-
return new
|
|
8184
|
+
return new ClaudeCliAnalysisProvider2({
|
|
7193
8185
|
command: this.config.agent.command,
|
|
7194
8186
|
...selModel !== void 0 && { defaultModel: selModel },
|
|
7195
8187
|
...this.config.intelligence?.requestTimeoutMs !== void 0 && {
|
|
@@ -7200,7 +8192,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7200
8192
|
const apiKey = provider.apiKey ?? this.config.agent.apiKey ?? "ollama";
|
|
7201
8193
|
const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
|
|
7202
8194
|
const intel = this.config.intelligence;
|
|
7203
|
-
return new
|
|
8195
|
+
return new OpenAICompatibleAnalysisProvider2({
|
|
7204
8196
|
apiKey,
|
|
7205
8197
|
baseUrl,
|
|
7206
8198
|
...selModel !== void 0 && { defaultModel: selModel },
|
|
@@ -7682,9 +8674,18 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7682
8674
|
issue,
|
|
7683
8675
|
attempt: attempt || 1
|
|
7684
8676
|
});
|
|
8677
|
+
const useCase = useCaseForBackendParam(issue, backend);
|
|
8678
|
+
let routedBackendName;
|
|
8679
|
+
if (this.overrideBackend !== null) {
|
|
8680
|
+
routedBackendName = this.overrideBackend.name;
|
|
8681
|
+
} else if (this.backendFactory !== null) {
|
|
8682
|
+
routedBackendName = this.backendFactory.resolveName(useCase);
|
|
8683
|
+
} else {
|
|
8684
|
+
routedBackendName = this.config.agent.routing?.default ?? this.config.agent.backend ?? "unknown";
|
|
8685
|
+
}
|
|
7685
8686
|
const session = {
|
|
7686
8687
|
sessionId: `pending-${Date.now()}`,
|
|
7687
|
-
backendName:
|
|
8688
|
+
backendName: routedBackendName,
|
|
7688
8689
|
agentPid: null,
|
|
7689
8690
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7690
8691
|
lastEvent: "Dispatching",
|
|
@@ -7711,11 +8712,23 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7711
8712
|
issue.id,
|
|
7712
8713
|
issue.externalId ?? null,
|
|
7713
8714
|
issue.identifier,
|
|
7714
|
-
|
|
8715
|
+
routedBackendName,
|
|
7715
8716
|
attempt ?? 1,
|
|
7716
8717
|
issue.title
|
|
7717
8718
|
);
|
|
7718
|
-
|
|
8719
|
+
let agentBackend;
|
|
8720
|
+
if (this.overrideBackend !== null) {
|
|
8721
|
+
agentBackend = this.overrideBackend;
|
|
8722
|
+
} else if (this.backendFactory !== null) {
|
|
8723
|
+
agentBackend = this.backendFactory.forUseCase(useCase);
|
|
8724
|
+
} else {
|
|
8725
|
+
throw new Error(
|
|
8726
|
+
`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.`
|
|
8727
|
+
);
|
|
8728
|
+
}
|
|
8729
|
+
const activeRunner = new AgentRunner(agentBackend, {
|
|
8730
|
+
maxTurns: this.config.agent.maxTurns
|
|
8731
|
+
});
|
|
7719
8732
|
this.runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, activeRunner);
|
|
7720
8733
|
} catch (error) {
|
|
7721
8734
|
this.logger.error(`Dispatch failed for ${issue.identifier}`, { error: String(error) });
|
|
@@ -7748,7 +8761,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7748
8761
|
}
|
|
7749
8762
|
}
|
|
7750
8763
|
runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, runner) {
|
|
7751
|
-
const activeRunner = runner
|
|
8764
|
+
const activeRunner = runner;
|
|
7752
8765
|
this.logger.info(`Starting background task for ${issue.identifier}`);
|
|
7753
8766
|
const abortController = new AbortController();
|
|
7754
8767
|
this.abortControllers.set(issue.id, { controller: abortController, pid: null });
|
|
@@ -7869,6 +8882,42 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7869
8882
|
this.emit("state_change", this.getSnapshot());
|
|
7870
8883
|
await this.dispatchIssue(issue, 1, "local");
|
|
7871
8884
|
}
|
|
8885
|
+
/**
|
|
8886
|
+
* Initialize the LocalModelResolver and intelligence pipeline.
|
|
8887
|
+
*
|
|
8888
|
+
* Runs the initial probe (so resolver state reflects server availability)
|
|
8889
|
+
* before constructing the intelligence pipeline. Subscribes the dashboard
|
|
8890
|
+
* broadcast stub to status changes. Called exactly once from start().
|
|
8891
|
+
*/
|
|
8892
|
+
async initLocalModelAndPipeline() {
|
|
8893
|
+
if (this.localResolvers.size > 0) {
|
|
8894
|
+
const backends = this.config.agent.backends ?? {};
|
|
8895
|
+
for (const [name, resolver] of this.localResolvers) {
|
|
8896
|
+
const def = backends[name];
|
|
8897
|
+
if (!def || def.type !== "local" && def.type !== "pi") {
|
|
8898
|
+
this.logger.warn("Resolver without matching backend def \u2014 broadcast skipped", {
|
|
8899
|
+
name
|
|
8900
|
+
});
|
|
8901
|
+
continue;
|
|
8902
|
+
}
|
|
8903
|
+
const endpoint = def.endpoint;
|
|
8904
|
+
const unsubscribe = resolver.onStatusChange((status) => {
|
|
8905
|
+
const named = {
|
|
8906
|
+
...status,
|
|
8907
|
+
backendName: name,
|
|
8908
|
+
endpoint
|
|
8909
|
+
};
|
|
8910
|
+
this.server?.broadcastLocalModelStatus(named);
|
|
8911
|
+
});
|
|
8912
|
+
this.localModelStatusUnsubscribes.push(unsubscribe);
|
|
8913
|
+
}
|
|
8914
|
+
for (const resolver of this.localResolvers.values()) {
|
|
8915
|
+
await resolver.start();
|
|
8916
|
+
}
|
|
8917
|
+
}
|
|
8918
|
+
this.pipeline = this.createIntelligencePipeline();
|
|
8919
|
+
this.server?.setPipeline(this.pipeline);
|
|
8920
|
+
}
|
|
7872
8921
|
/**
|
|
7873
8922
|
* Starts the polling loop and the internal HTTP server.
|
|
7874
8923
|
* Runs startup reconciliation to release orphaned claims before the first tick.
|
|
@@ -7877,6 +8926,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7877
8926
|
if (this.server) {
|
|
7878
8927
|
void this.server.start();
|
|
7879
8928
|
}
|
|
8929
|
+
await this.initLocalModelAndPipeline();
|
|
7880
8930
|
await this.ensureClaimManager();
|
|
7881
8931
|
const runningIssueIds = new Set(this.state.running.keys());
|
|
7882
8932
|
const reconcileResult = await this.claimManager.reconcileOnStartup(runningIssueIds);
|
|
@@ -7928,6 +8978,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7928
8978
|
clearInterval(this.heartbeatInterval);
|
|
7929
8979
|
this.heartbeatInterval = void 0;
|
|
7930
8980
|
}
|
|
8981
|
+
for (const unsub of this.localModelStatusUnsubscribes) {
|
|
8982
|
+
unsub();
|
|
8983
|
+
}
|
|
8984
|
+
this.localModelStatusUnsubscribes = [];
|
|
8985
|
+
for (const resolver of this.localResolvers.values()) {
|
|
8986
|
+
resolver.stop();
|
|
8987
|
+
}
|
|
7931
8988
|
if (this.maintenanceScheduler) {
|
|
7932
8989
|
this.maintenanceScheduler.stop();
|
|
7933
8990
|
this.maintenanceScheduler = null;
|
|
@@ -8204,12 +9261,14 @@ function launchTUI(orchestrator) {
|
|
|
8204
9261
|
}
|
|
8205
9262
|
export {
|
|
8206
9263
|
AnalysisArchive,
|
|
9264
|
+
BackendRouter,
|
|
8207
9265
|
ClaimManager,
|
|
8208
9266
|
InteractionQueue,
|
|
8209
9267
|
LinearGraphQLStub,
|
|
8210
9268
|
MockBackend,
|
|
8211
9269
|
ORCHESTRATOR_IDENTITY_FILE,
|
|
8212
9270
|
Orchestrator,
|
|
9271
|
+
OrchestratorBackendFactory,
|
|
8213
9272
|
PRDetector,
|
|
8214
9273
|
PromptRenderer,
|
|
8215
9274
|
RoadmapTrackerAdapter,
|
|
@@ -8222,6 +9281,7 @@ export {
|
|
|
8222
9281
|
calculateRetryDelay,
|
|
8223
9282
|
canDispatch,
|
|
8224
9283
|
computeRateLimitDelay,
|
|
9284
|
+
createBackend,
|
|
8225
9285
|
createEmptyState,
|
|
8226
9286
|
detectScopeTier,
|
|
8227
9287
|
extractHighlights,
|
|
@@ -8232,6 +9292,7 @@ export {
|
|
|
8232
9292
|
isEligible,
|
|
8233
9293
|
launchTUI,
|
|
8234
9294
|
loadPublishedIndex,
|
|
9295
|
+
migrateAgentConfig,
|
|
8235
9296
|
reconcile,
|
|
8236
9297
|
renderAnalysisComment,
|
|
8237
9298
|
renderPRComment,
|