opencode-ultra 0.8.3 → 0.9.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/config.d.ts +1 -0
- package/dist/index.js +724 -180
- package/dist/shared/concurrency.d.ts +109 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/tools/health-check.d.ts +73 -0
- package/dist/tools/ralph-loop.d.ts +16 -0
- package/dist/tools/spawn-agent.d.ts +19 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -76,6 +76,7 @@ declare const PluginConfigSchema: z.ZodObject<{
|
|
|
76
76
|
evolve_exe: z.ZodOptional<z.ZodObject<{
|
|
77
77
|
maxIterations: z.ZodOptional<z.ZodNumber>;
|
|
78
78
|
iterationTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
totalTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
79
80
|
skipReview: z.ZodOptional<z.ZodBoolean>;
|
|
80
81
|
skipTests: z.ZodOptional<z.ZodBoolean>;
|
|
81
82
|
}, z.core.$strip>>;
|
package/dist/index.js
CHANGED
|
@@ -14415,6 +14415,228 @@ class TtlMap {
|
|
|
14415
14415
|
this.map.clear();
|
|
14416
14416
|
}
|
|
14417
14417
|
}
|
|
14418
|
+
// src/shared/concurrency.ts
|
|
14419
|
+
function computeExponentialBackoffDelayMs(attempt, opts = {}) {
|
|
14420
|
+
const base = opts.baseDelayMs ?? 200;
|
|
14421
|
+
const max = opts.maxDelayMs ?? 1e4;
|
|
14422
|
+
const jitter = opts.jitter ?? "full";
|
|
14423
|
+
const rng = opts.rng ?? Math.random;
|
|
14424
|
+
const a = Math.max(0, Math.floor(attempt));
|
|
14425
|
+
const cap = Math.min(max, base * 2 ** a);
|
|
14426
|
+
if (jitter === "none")
|
|
14427
|
+
return Math.floor(cap);
|
|
14428
|
+
const r0 = rng();
|
|
14429
|
+
const r = Number.isFinite(r0) ? Math.max(0, Math.min(0.999999, r0)) : 0;
|
|
14430
|
+
return Math.floor(r * cap);
|
|
14431
|
+
}
|
|
14432
|
+
class CircuitBreaker {
|
|
14433
|
+
state = "closed";
|
|
14434
|
+
consecutiveFailures = 0;
|
|
14435
|
+
openUntil = 0;
|
|
14436
|
+
halfOpenInFlight = false;
|
|
14437
|
+
failureThreshold;
|
|
14438
|
+
cooldownMs;
|
|
14439
|
+
constructor(opts = {}) {
|
|
14440
|
+
this.failureThreshold = Math.max(1, Math.floor(opts.failureThreshold ?? 3));
|
|
14441
|
+
this.cooldownMs = Math.max(0, Math.floor(opts.cooldownMs ?? 1e4));
|
|
14442
|
+
}
|
|
14443
|
+
snapshot(now) {
|
|
14444
|
+
const openForMs = this.state === "open" ? Math.max(0, this.openUntil - now) : 0;
|
|
14445
|
+
return { state: this.state, consecutiveFailures: this.consecutiveFailures, openForMs };
|
|
14446
|
+
}
|
|
14447
|
+
enter(now) {
|
|
14448
|
+
if (this.state === "open") {
|
|
14449
|
+
if (now < this.openUntil) {
|
|
14450
|
+
return { allowed: false, waitMs: this.openUntil - now };
|
|
14451
|
+
}
|
|
14452
|
+
this.state = "half_open";
|
|
14453
|
+
this.halfOpenInFlight = false;
|
|
14454
|
+
}
|
|
14455
|
+
if (this.state === "half_open") {
|
|
14456
|
+
if (this.halfOpenInFlight)
|
|
14457
|
+
return { allowed: false, waitMs: this.cooldownMs };
|
|
14458
|
+
this.halfOpenInFlight = true;
|
|
14459
|
+
return { allowed: true };
|
|
14460
|
+
}
|
|
14461
|
+
return { allowed: true };
|
|
14462
|
+
}
|
|
14463
|
+
onSuccess() {
|
|
14464
|
+
this.consecutiveFailures = 0;
|
|
14465
|
+
this.openUntil = 0;
|
|
14466
|
+
this.halfOpenInFlight = false;
|
|
14467
|
+
this.state = "closed";
|
|
14468
|
+
}
|
|
14469
|
+
onFailure(now) {
|
|
14470
|
+
this.halfOpenInFlight = false;
|
|
14471
|
+
if (this.state === "half_open") {
|
|
14472
|
+
this.state = "open";
|
|
14473
|
+
this.openUntil = now + this.cooldownMs;
|
|
14474
|
+
this.consecutiveFailures = this.failureThreshold;
|
|
14475
|
+
return;
|
|
14476
|
+
}
|
|
14477
|
+
this.consecutiveFailures++;
|
|
14478
|
+
if (this.consecutiveFailures >= this.failureThreshold) {
|
|
14479
|
+
this.state = "open";
|
|
14480
|
+
this.openUntil = now + this.cooldownMs;
|
|
14481
|
+
}
|
|
14482
|
+
}
|
|
14483
|
+
}
|
|
14484
|
+
|
|
14485
|
+
class RetryBudgetExceededError extends Error {
|
|
14486
|
+
provider;
|
|
14487
|
+
constructor(provider) {
|
|
14488
|
+
super(`Retry budget exceeded for ${provider}`);
|
|
14489
|
+
this.name = "RetryBudgetExceededError";
|
|
14490
|
+
this.provider = provider;
|
|
14491
|
+
}
|
|
14492
|
+
}
|
|
14493
|
+
|
|
14494
|
+
class RetryBudget {
|
|
14495
|
+
maxRetries;
|
|
14496
|
+
intervalMs;
|
|
14497
|
+
state;
|
|
14498
|
+
constructor(opts = {}) {
|
|
14499
|
+
this.maxRetries = Math.max(0, Math.floor(opts.maxRetries ?? 20));
|
|
14500
|
+
this.intervalMs = Math.max(1, Math.floor(opts.intervalMs ?? 60000));
|
|
14501
|
+
this.state = new TtlMap({ maxSize: 1000, ttlMs: this.intervalMs });
|
|
14502
|
+
}
|
|
14503
|
+
trySpend(provider, cost = 1) {
|
|
14504
|
+
const c = Math.max(0, Math.floor(cost));
|
|
14505
|
+
if (c === 0)
|
|
14506
|
+
return true;
|
|
14507
|
+
const entry = this.state.get(provider) ?? { used: 0 };
|
|
14508
|
+
if (entry.used + c > this.maxRetries)
|
|
14509
|
+
return false;
|
|
14510
|
+
this.state.set(provider, { used: entry.used + c });
|
|
14511
|
+
return true;
|
|
14512
|
+
}
|
|
14513
|
+
}
|
|
14514
|
+
function getHeaderValue(headers, name) {
|
|
14515
|
+
if (!headers || typeof headers !== "object")
|
|
14516
|
+
return;
|
|
14517
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
14518
|
+
if (!key)
|
|
14519
|
+
return;
|
|
14520
|
+
const v = headers[key];
|
|
14521
|
+
return typeof v === "string" ? v : Array.isArray(v) && typeof v[0] === "string" ? v[0] : undefined;
|
|
14522
|
+
}
|
|
14523
|
+
function parseRetryAfterMs(value, now) {
|
|
14524
|
+
const trimmed = value.trim();
|
|
14525
|
+
if (!trimmed)
|
|
14526
|
+
return;
|
|
14527
|
+
const asNum = Number(trimmed);
|
|
14528
|
+
if (Number.isFinite(asNum) && asNum >= 0)
|
|
14529
|
+
return Math.floor(asNum * 1000);
|
|
14530
|
+
const asDate = Date.parse(trimmed);
|
|
14531
|
+
if (!Number.isNaN(asDate)) {
|
|
14532
|
+
const ms = asDate - now;
|
|
14533
|
+
return ms > 0 ? ms : 0;
|
|
14534
|
+
}
|
|
14535
|
+
return;
|
|
14536
|
+
}
|
|
14537
|
+
function classifyRetryableError(err, now = Date.now()) {
|
|
14538
|
+
const anyErr = err;
|
|
14539
|
+
const status = typeof anyErr?.status === "number" ? anyErr.status : typeof anyErr?.statusCode === "number" ? anyErr.statusCode : typeof anyErr?.response?.status === "number" ? anyErr.response.status : typeof anyErr?.response?.statusCode === "number" ? anyErr.response.statusCode : undefined;
|
|
14540
|
+
const headers = anyErr?.headers ?? anyErr?.response?.headers;
|
|
14541
|
+
const retryAfterRaw = getHeaderValue(headers, "retry-after");
|
|
14542
|
+
const retryAfterMs = retryAfterRaw ? parseRetryAfterMs(retryAfterRaw, now) : undefined;
|
|
14543
|
+
if (status === 429)
|
|
14544
|
+
return { retryable: true, status, retryAfterMs };
|
|
14545
|
+
if (typeof status === "number" && status >= 500 && status <= 599)
|
|
14546
|
+
return { retryable: true, status, retryAfterMs };
|
|
14547
|
+
const code = typeof anyErr?.code === "string" ? anyErr.code : undefined;
|
|
14548
|
+
if (code && (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "EAI_AGAIN" || code === "ENOTFOUND")) {
|
|
14549
|
+
return { retryable: true };
|
|
14550
|
+
}
|
|
14551
|
+
return { retryable: false, status, retryAfterMs };
|
|
14552
|
+
}
|
|
14553
|
+
async function adaptiveRetry(provider, fn, breaker, budget, opts = {}) {
|
|
14554
|
+
const maxAttempts = Math.max(1, Math.floor(opts.maxAttempts ?? 4));
|
|
14555
|
+
const nowFn = opts.now ?? Date.now;
|
|
14556
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
14557
|
+
const classify = opts.classify ?? classifyRetryableError;
|
|
14558
|
+
const maxRetryAfterMs = Math.max(0, Math.floor(opts.maxRetryAfterMs ?? 30000));
|
|
14559
|
+
const backoff = opts.backoff ?? {};
|
|
14560
|
+
let attempt = 0;
|
|
14561
|
+
while (true) {
|
|
14562
|
+
const now = nowFn();
|
|
14563
|
+
const gate = breaker.enter(now);
|
|
14564
|
+
if (!gate.allowed) {
|
|
14565
|
+
const waitMs = Math.max(0, Math.floor(gate.waitMs ?? 0));
|
|
14566
|
+
if (waitMs > 0) {
|
|
14567
|
+
const delay = Math.min(waitMs, maxRetryAfterMs);
|
|
14568
|
+
await sleep(delay);
|
|
14569
|
+
}
|
|
14570
|
+
}
|
|
14571
|
+
try {
|
|
14572
|
+
const result = await fn();
|
|
14573
|
+
breaker.onSuccess();
|
|
14574
|
+
return result;
|
|
14575
|
+
} catch (err) {
|
|
14576
|
+
const now2 = nowFn();
|
|
14577
|
+
const decision = classify(err, now2);
|
|
14578
|
+
if (!decision.retryable) {
|
|
14579
|
+
throw err;
|
|
14580
|
+
}
|
|
14581
|
+
breaker.onFailure(now2);
|
|
14582
|
+
attempt++;
|
|
14583
|
+
if (attempt >= maxAttempts)
|
|
14584
|
+
throw err;
|
|
14585
|
+
if (!budget.trySpend(provider, 1)) {
|
|
14586
|
+
throw new RetryBudgetExceededError(provider);
|
|
14587
|
+
}
|
|
14588
|
+
const retryAfterMs = typeof decision.retryAfterMs === "number" ? Math.min(Math.max(0, decision.retryAfterMs), maxRetryAfterMs) : undefined;
|
|
14589
|
+
const delay = retryAfterMs !== undefined ? retryAfterMs : computeExponentialBackoffDelayMs(attempt - 1, {
|
|
14590
|
+
baseDelayMs: backoff.baseDelayMs,
|
|
14591
|
+
maxDelayMs: backoff.maxDelayMs,
|
|
14592
|
+
jitter: backoff.jitter,
|
|
14593
|
+
rng: backoff.rng
|
|
14594
|
+
});
|
|
14595
|
+
if (delay > 0)
|
|
14596
|
+
await sleep(delay);
|
|
14597
|
+
}
|
|
14598
|
+
}
|
|
14599
|
+
}
|
|
14600
|
+
|
|
14601
|
+
class ProviderResilience {
|
|
14602
|
+
breakers = new Map;
|
|
14603
|
+
budgets = new Map;
|
|
14604
|
+
opts;
|
|
14605
|
+
constructor(opts = {}) {
|
|
14606
|
+
this.opts = opts;
|
|
14607
|
+
}
|
|
14608
|
+
getBreaker(provider) {
|
|
14609
|
+
const existing = this.breakers.get(provider);
|
|
14610
|
+
if (existing)
|
|
14611
|
+
return existing;
|
|
14612
|
+
const b = new CircuitBreaker(this.opts.breaker);
|
|
14613
|
+
this.breakers.set(provider, b);
|
|
14614
|
+
return b;
|
|
14615
|
+
}
|
|
14616
|
+
getBudget(provider) {
|
|
14617
|
+
const existing = this.budgets.get(provider);
|
|
14618
|
+
if (existing)
|
|
14619
|
+
return existing;
|
|
14620
|
+
const b = new RetryBudget(this.opts.retryBudget);
|
|
14621
|
+
this.budgets.set(provider, b);
|
|
14622
|
+
return b;
|
|
14623
|
+
}
|
|
14624
|
+
async run(provider, fn, retryOverrides = {}) {
|
|
14625
|
+
const breaker = this.getBreaker(provider);
|
|
14626
|
+
const budget = this.getBudget(provider);
|
|
14627
|
+
const baseRetry = this.opts.retry ?? {};
|
|
14628
|
+
const merged = {
|
|
14629
|
+
...baseRetry,
|
|
14630
|
+
...retryOverrides,
|
|
14631
|
+
backoff: { ...baseRetry.backoff, ...retryOverrides.backoff }
|
|
14632
|
+
};
|
|
14633
|
+
return adaptiveRetry(provider, fn, breaker, budget, merged);
|
|
14634
|
+
}
|
|
14635
|
+
snapshot(provider, now = Date.now()) {
|
|
14636
|
+
const breaker = this.getBreaker(provider);
|
|
14637
|
+
return { breaker: breaker.snapshot(now) };
|
|
14638
|
+
}
|
|
14639
|
+
}
|
|
14418
14640
|
// src/config.ts
|
|
14419
14641
|
var AgentOverrideSchema = exports_external.object({
|
|
14420
14642
|
model: exports_external.string().optional(),
|
|
@@ -14476,6 +14698,7 @@ var PluginConfigSchema = exports_external.object({
|
|
|
14476
14698
|
evolve_exe: exports_external.object({
|
|
14477
14699
|
maxIterations: exports_external.number().min(1).max(20).optional(),
|
|
14478
14700
|
iterationTimeoutMs: exports_external.number().min(1e4).optional(),
|
|
14701
|
+
totalTimeoutMs: exports_external.number().min(60000).optional(),
|
|
14479
14702
|
skipReview: exports_external.boolean().optional(),
|
|
14480
14703
|
skipTests: exports_external.boolean().optional()
|
|
14481
14704
|
}).optional()
|
|
@@ -27549,6 +27772,86 @@ function tool(input) {
|
|
|
27549
27772
|
return input;
|
|
27550
27773
|
}
|
|
27551
27774
|
tool.schema = exports_external2;
|
|
27775
|
+
// src/concurrency/semaphore.ts
|
|
27776
|
+
class Semaphore {
|
|
27777
|
+
max;
|
|
27778
|
+
queue = [];
|
|
27779
|
+
active = 0;
|
|
27780
|
+
constructor(max) {
|
|
27781
|
+
this.max = max;
|
|
27782
|
+
}
|
|
27783
|
+
async acquire() {
|
|
27784
|
+
if (this.active < this.max) {
|
|
27785
|
+
this.active++;
|
|
27786
|
+
return;
|
|
27787
|
+
}
|
|
27788
|
+
return new Promise((resolve2) => {
|
|
27789
|
+
this.queue.push(() => {
|
|
27790
|
+
this.active++;
|
|
27791
|
+
resolve2();
|
|
27792
|
+
});
|
|
27793
|
+
});
|
|
27794
|
+
}
|
|
27795
|
+
release() {
|
|
27796
|
+
if (this.active <= 0)
|
|
27797
|
+
return;
|
|
27798
|
+
this.active--;
|
|
27799
|
+
const next = this.queue.shift();
|
|
27800
|
+
if (next)
|
|
27801
|
+
next();
|
|
27802
|
+
}
|
|
27803
|
+
get pending() {
|
|
27804
|
+
return this.queue.length;
|
|
27805
|
+
}
|
|
27806
|
+
get running() {
|
|
27807
|
+
return this.active;
|
|
27808
|
+
}
|
|
27809
|
+
}
|
|
27810
|
+
// src/concurrency/pool.ts
|
|
27811
|
+
function extractProvider(model) {
|
|
27812
|
+
const slash = model.indexOf("/");
|
|
27813
|
+
return slash > 0 ? model.slice(0, slash) : model;
|
|
27814
|
+
}
|
|
27815
|
+
|
|
27816
|
+
class ConcurrencyPool {
|
|
27817
|
+
global;
|
|
27818
|
+
providers = new Map;
|
|
27819
|
+
models = new Map;
|
|
27820
|
+
config;
|
|
27821
|
+
constructor(config3) {
|
|
27822
|
+
this.config = config3;
|
|
27823
|
+
this.global = new Semaphore(config3.defaultConcurrency ?? Infinity);
|
|
27824
|
+
for (const [provider, limit] of Object.entries(config3.providerConcurrency ?? {})) {
|
|
27825
|
+
this.providers.set(provider, new Semaphore(limit));
|
|
27826
|
+
}
|
|
27827
|
+
for (const [model, limit] of Object.entries(config3.modelConcurrency ?? {})) {
|
|
27828
|
+
this.models.set(model, new Semaphore(limit));
|
|
27829
|
+
}
|
|
27830
|
+
}
|
|
27831
|
+
async run(model, fn) {
|
|
27832
|
+
const provider = extractProvider(model);
|
|
27833
|
+
const semaphores = [];
|
|
27834
|
+
const modelSem = this.models.get(model);
|
|
27835
|
+
if (modelSem) {
|
|
27836
|
+
await modelSem.acquire();
|
|
27837
|
+
semaphores.push(modelSem);
|
|
27838
|
+
}
|
|
27839
|
+
const providerSem = this.providers.get(provider);
|
|
27840
|
+
if (providerSem) {
|
|
27841
|
+
await providerSem.acquire();
|
|
27842
|
+
semaphores.push(providerSem);
|
|
27843
|
+
}
|
|
27844
|
+
await this.global.acquire();
|
|
27845
|
+
semaphores.push(this.global);
|
|
27846
|
+
try {
|
|
27847
|
+
return await fn();
|
|
27848
|
+
} finally {
|
|
27849
|
+
for (let i = semaphores.length - 1;i >= 0; i--) {
|
|
27850
|
+
semaphores[i].release();
|
|
27851
|
+
}
|
|
27852
|
+
}
|
|
27853
|
+
}
|
|
27854
|
+
}
|
|
27552
27855
|
// src/categories/index.ts
|
|
27553
27856
|
var DEFAULT_CATEGORIES = {
|
|
27554
27857
|
"visual-engineering": {
|
|
@@ -27900,7 +28203,7 @@ async function withTimeout(promise3, ms, label) {
|
|
|
27900
28203
|
clearTimeout(timer);
|
|
27901
28204
|
}
|
|
27902
28205
|
}
|
|
27903
|
-
async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
|
|
28206
|
+
async function runAgent(ctx, task, toolCtx, internalSessions, resilience, provider, deps, progress) {
|
|
27904
28207
|
const { agent, prompt, description } = task;
|
|
27905
28208
|
const t0 = Date.now();
|
|
27906
28209
|
const timeoutMs = deps.agentTimeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
|
|
@@ -27916,10 +28219,10 @@ async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
|
|
|
27916
28219
|
log(`spawn_agent: starting ${agent}`, { description });
|
|
27917
28220
|
let sessionID;
|
|
27918
28221
|
try {
|
|
27919
|
-
const sessionResp = await ctx.client.session.create({
|
|
28222
|
+
const sessionResp = await resilience.run(provider, () => ctx.client.session.create({
|
|
27920
28223
|
body: {},
|
|
27921
28224
|
query: { directory: ctx.directory }
|
|
27922
|
-
});
|
|
28225
|
+
}), { maxAttempts: 3 });
|
|
27923
28226
|
sessionID = sessionResp.data?.id;
|
|
27924
28227
|
if (!sessionID) {
|
|
27925
28228
|
log(`spawn_agent: ${agent} \u2014 failed to create session`);
|
|
@@ -27928,25 +28231,26 @@ async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
|
|
|
27928
28231
|
**Agent**: ${agent}
|
|
27929
28232
|
**Error**: Failed to create session`;
|
|
27930
28233
|
}
|
|
27931
|
-
|
|
27932
|
-
|
|
27933
|
-
|
|
28234
|
+
const id = sessionID;
|
|
28235
|
+
internalSessions.add(id);
|
|
28236
|
+
await resilience.run(provider, () => withTimeout(ctx.client.session.prompt({
|
|
28237
|
+
path: { id },
|
|
27934
28238
|
body: {
|
|
27935
28239
|
parts: [{ type: "text", text: prompt }],
|
|
27936
28240
|
agent
|
|
27937
28241
|
},
|
|
27938
28242
|
query: { directory: ctx.directory }
|
|
27939
|
-
}), timeoutMs, `${agent} (${description})`);
|
|
27940
|
-
const messagesResp = await ctx.client.session.messages({
|
|
27941
|
-
path: { id
|
|
28243
|
+
}), timeoutMs, `${agent} (${description})`), { maxAttempts: 4 });
|
|
28244
|
+
const messagesResp = await resilience.run(provider, () => ctx.client.session.messages({
|
|
28245
|
+
path: { id },
|
|
27942
28246
|
query: { directory: ctx.directory }
|
|
27943
|
-
});
|
|
28247
|
+
}), { maxAttempts: 3 });
|
|
27944
28248
|
const messages = messagesResp.data ?? [];
|
|
27945
28249
|
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
27946
28250
|
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
27947
28251
|
`) ?? "(No response from agent)";
|
|
27948
|
-
internalSessions.delete(
|
|
27949
|
-
await ctx.client.session.delete({ path: { id
|
|
28252
|
+
internalSessions.delete(id);
|
|
28253
|
+
await ctx.client.session.delete({ path: { id }, query: { directory: ctx.directory } }).catch(() => {});
|
|
27950
28254
|
const agentTime = ((Date.now() - t0) / 1000).toFixed(1);
|
|
27951
28255
|
log(`spawn_agent: ${agent} done`, { seconds: agentTime, description });
|
|
27952
28256
|
const result = sanitizeSpawnResult(rawResult);
|
|
@@ -27979,6 +28283,11 @@ ${result}`;
|
|
|
27979
28283
|
}
|
|
27980
28284
|
function createSpawnAgentTool(ctx, internalSessions, deps = {}) {
|
|
27981
28285
|
const maxTotalSpawned = deps.maxTotalSpawned ?? DEFAULT_MAX_TOTAL_SPAWNED;
|
|
28286
|
+
const resilience = deps.resilience ?? new ProviderResilience({
|
|
28287
|
+
breaker: { failureThreshold: 3, cooldownMs: 1e4 },
|
|
28288
|
+
retryBudget: { maxRetries: 20, intervalMs: 60000 },
|
|
28289
|
+
retry: { maxAttempts: 4, backoff: { baseDelayMs: 200, maxDelayMs: 1e4, jitter: "full" } }
|
|
28290
|
+
});
|
|
27982
28291
|
return tool({
|
|
27983
28292
|
description: `Spawn subagents to execute tasks in PARALLEL.
|
|
27984
28293
|
All agents in the array run concurrently (respecting concurrency limits if configured).
|
|
@@ -28043,7 +28352,8 @@ spawn_agent({
|
|
|
28043
28352
|
const task = agents[0];
|
|
28044
28353
|
toolCtx.metadata({ title: `${task.agent}: ${task.description}...` });
|
|
28045
28354
|
const model = resolveTaskModel(task, deps);
|
|
28046
|
-
const
|
|
28355
|
+
const provider = model ? extractProvider(model) : "unknown";
|
|
28356
|
+
const execute = () => runAgent(ctx, task, toolCtx, internalSessions, resilience, provider, deps);
|
|
28047
28357
|
let result;
|
|
28048
28358
|
if (deps.pool && model) {
|
|
28049
28359
|
result = await deps.pool.run(model, execute);
|
|
@@ -28060,7 +28370,8 @@ spawn_agent({
|
|
|
28060
28370
|
});
|
|
28061
28371
|
const tasks = agents.map(async (task) => {
|
|
28062
28372
|
const model = resolveTaskModel(task, deps);
|
|
28063
|
-
const
|
|
28373
|
+
const provider = model ? extractProvider(model) : "unknown";
|
|
28374
|
+
const execute = () => runAgent(ctx, task, toolCtx, internalSessions, resilience, provider, deps, progress);
|
|
28064
28375
|
let result;
|
|
28065
28376
|
if (deps.pool && model) {
|
|
28066
28377
|
result = await deps.pool.run(model, execute);
|
|
@@ -28106,6 +28417,14 @@ var COMPLETION_MARKER = "<promise>DONE</promise>";
|
|
|
28106
28417
|
var DEFAULT_MAX_ITERATIONS = 10;
|
|
28107
28418
|
var DEFAULT_ITERATION_TIMEOUT_MS = 180000;
|
|
28108
28419
|
var activeLoops = new Map;
|
|
28420
|
+
function getActiveRalphLoops() {
|
|
28421
|
+
return [...activeLoops.values()].map((s) => ({
|
|
28422
|
+
sessionID: s.sessionID,
|
|
28423
|
+
iteration: s.iteration,
|
|
28424
|
+
maxIterations: s.maxIterations,
|
|
28425
|
+
active: s.active
|
|
28426
|
+
}));
|
|
28427
|
+
}
|
|
28109
28428
|
async function withTimeout2(promise3, ms, label) {
|
|
28110
28429
|
let timer;
|
|
28111
28430
|
const timeout = new Promise((_, reject) => {
|
|
@@ -29083,8 +29402,15 @@ import { spawnSync as spawnSync2 } from "child_process";
|
|
|
29083
29402
|
import * as fs9 from "fs";
|
|
29084
29403
|
import * as path9 from "path";
|
|
29085
29404
|
var COMPLETION_MARKER2 = "<promise>DONE</promise>";
|
|
29086
|
-
var
|
|
29087
|
-
|
|
29405
|
+
var COMPLETION_PATTERNS = [
|
|
29406
|
+
COMPLETION_MARKER2,
|
|
29407
|
+
"<promise>done</promise>",
|
|
29408
|
+
"IMPLEMENTATION COMPLETE",
|
|
29409
|
+
"Implementation complete"
|
|
29410
|
+
];
|
|
29411
|
+
var DEFAULT_MAX_ITERATIONS2 = 5;
|
|
29412
|
+
var DEFAULT_ITERATION_TIMEOUT_MS2 = 120000;
|
|
29413
|
+
var DEFAULT_TOTAL_TIMEOUT_MS = 600000;
|
|
29088
29414
|
async function withTimeout3(promise3, ms, label) {
|
|
29089
29415
|
let timer;
|
|
29090
29416
|
const timeout = new Promise((_, reject) => {
|
|
@@ -29170,36 +29496,34 @@ function buildProjectInfo(cwd) {
|
|
|
29170
29496
|
}
|
|
29171
29497
|
}
|
|
29172
29498
|
var IMPLEMENT_PROMPT = (proposal, projectInfo) => `
|
|
29173
|
-
[EVOLVE
|
|
29499
|
+
[EVOLVE \u2014 AUTONOMOUS IMPLEMENTATION]
|
|
29174
29500
|
|
|
29175
|
-
|
|
29176
|
-
Implement the following improvement proposal for this project:
|
|
29501
|
+
You MUST complete this in a SINGLE iteration. Do not wait for follow-up prompts.
|
|
29177
29502
|
|
|
29178
|
-
|
|
29179
|
-
|
|
29180
|
-
**
|
|
29181
|
-
${proposal.
|
|
29182
|
-
${proposal.
|
|
29183
|
-
${proposal.
|
|
29503
|
+
## Proposal
|
|
29504
|
+
- **Title**: ${proposal.title}
|
|
29505
|
+
- **Priority**: ${proposal.priority} | **Effort**: ${proposal.effort}
|
|
29506
|
+
${proposal.description ? `- **What to do**: ${proposal.description}` : ""}
|
|
29507
|
+
${proposal.currentState ? `- **Current state**: ${proposal.currentState}` : ""}
|
|
29508
|
+
${proposal.files?.length ? `- **Target files**: ${proposal.files.join(", ")}` : ""}
|
|
29184
29509
|
|
|
29185
|
-
## Project
|
|
29510
|
+
## Project
|
|
29186
29511
|
${projectInfo}
|
|
29187
29512
|
|
|
29188
|
-
##
|
|
29189
|
-
1.
|
|
29190
|
-
2. Write
|
|
29191
|
-
3.
|
|
29192
|
-
4.
|
|
29193
|
-
|
|
29194
|
-
|
|
29195
|
-
|
|
29196
|
-
|
|
29197
|
-
|
|
29198
|
-
|
|
29199
|
-
6. Do NOT add unnecessary dependencies
|
|
29513
|
+
## Steps (execute ALL in order)
|
|
29514
|
+
1. Read the target files to understand current code
|
|
29515
|
+
2. Write the implementation (new files or edits)
|
|
29516
|
+
3. Write tests in \`__test__/\` matching existing patterns
|
|
29517
|
+
4. Verify your changes compile (no syntax errors)
|
|
29518
|
+
|
|
29519
|
+
## Rules
|
|
29520
|
+
- Do NOT touch unrelated files
|
|
29521
|
+
- Do NOT add dependencies
|
|
29522
|
+
- Follow existing code patterns (TypeScript strict, pure functions, zod schemas)
|
|
29523
|
+
- Keep changes minimal and focused
|
|
29200
29524
|
|
|
29201
|
-
##
|
|
29202
|
-
|
|
29525
|
+
## When done
|
|
29526
|
+
Output exactly: ${COMPLETION_MARKER2}
|
|
29203
29527
|
`;
|
|
29204
29528
|
var REVIEW_PROMPT = (proposal, diff, testOutput) => `
|
|
29205
29529
|
[CODE REVIEW \u2014 EVOLVE IMPLEMENTATION]
|
|
@@ -29231,21 +29555,24 @@ Then overall: APPROVE / APPROVE_WITH_WARNINGS / BLOCK
|
|
|
29231
29555
|
|
|
29232
29556
|
If BLOCK: explain what must be fixed before merging.
|
|
29233
29557
|
`;
|
|
29234
|
-
function buildContinuationPrompt2(
|
|
29235
|
-
return `[Evolve Exe \u2014 Iteration ${iteration}]
|
|
29558
|
+
function buildContinuationPrompt2(_original, iteration, maxIterations) {
|
|
29559
|
+
return `[Evolve Exe \u2014 Iteration ${iteration}/${maxIterations}]
|
|
29236
29560
|
|
|
29237
|
-
|
|
29238
|
-
- Review your progress so far
|
|
29239
|
-
- Continue from where you left off
|
|
29240
|
-
- When FULLY complete, output exactly: ${COMPLETION_MARKER2}
|
|
29241
|
-
- Do not stop until the task is truly done
|
|
29561
|
+
Continue from where you left off. This is iteration ${iteration} of ${maxIterations} \u2014 if you do not finish now, the implementation will be rolled back.
|
|
29242
29562
|
|
|
29243
|
-
|
|
29244
|
-
|
|
29563
|
+
Finish all remaining work and output: ${COMPLETION_MARKER2}`;
|
|
29564
|
+
}
|
|
29565
|
+
function isCompletionDetected(text) {
|
|
29566
|
+
return COMPLETION_PATTERNS.some((p) => text.includes(p));
|
|
29567
|
+
}
|
|
29568
|
+
function getFileChanges(cwd) {
|
|
29569
|
+
const result = spawnSync2("git", ["diff", "--stat"], { cwd, encoding: "utf-8", timeout: 1e4 });
|
|
29570
|
+
return (result.stdout ?? "").trim();
|
|
29245
29571
|
}
|
|
29246
29572
|
function createEvolveExeTool(ctx, internalSessions, deps = {}) {
|
|
29247
29573
|
const maxIterations = deps.evolveExeConfig?.maxIterations ?? DEFAULT_MAX_ITERATIONS2;
|
|
29248
29574
|
const iterationTimeoutMs = deps.evolveExeConfig?.iterationTimeoutMs ?? deps.agentTimeoutMs ?? DEFAULT_ITERATION_TIMEOUT_MS2;
|
|
29575
|
+
const totalTimeoutMs = deps.evolveExeConfig?.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS;
|
|
29249
29576
|
const configSkipReview = deps.evolveExeConfig?.skipReview ?? false;
|
|
29250
29577
|
const configSkipTests = deps.evolveExeConfig?.skipTests ?? false;
|
|
29251
29578
|
return tool({
|
|
@@ -29337,6 +29664,7 @@ Failed to determine current git branch. Is this a git repository?`;
|
|
|
29337
29664
|
skipReview,
|
|
29338
29665
|
maxIterations,
|
|
29339
29666
|
iterationTimeoutMs,
|
|
29667
|
+
totalTimeoutMs,
|
|
29340
29668
|
progressLabel: `[${i + 1}/${selected.length}]`
|
|
29341
29669
|
});
|
|
29342
29670
|
results.push(result);
|
|
@@ -29381,6 +29709,7 @@ async function executeProposal(args) {
|
|
|
29381
29709
|
skipReview,
|
|
29382
29710
|
maxIterations,
|
|
29383
29711
|
iterationTimeoutMs,
|
|
29712
|
+
totalTimeoutMs,
|
|
29384
29713
|
progressLabel
|
|
29385
29714
|
} = args;
|
|
29386
29715
|
const slug = slugify2(proposal.title);
|
|
@@ -29408,6 +29737,7 @@ async function executeProposal(args) {
|
|
|
29408
29737
|
projectInfo,
|
|
29409
29738
|
maxIterations,
|
|
29410
29739
|
iterationTimeoutMs,
|
|
29740
|
+
totalTimeoutMs,
|
|
29411
29741
|
toolCtx,
|
|
29412
29742
|
progressLabel
|
|
29413
29743
|
});
|
|
@@ -29477,7 +29807,8 @@ async function rollback(cwd, originalBranch, branchName) {
|
|
|
29477
29807
|
log(`evolve_exe: rolled back branch ${branchName}`);
|
|
29478
29808
|
}
|
|
29479
29809
|
async function runImplementation(args) {
|
|
29480
|
-
const { ctx, internalSessions, proposal, projectInfo, maxIterations, iterationTimeoutMs, toolCtx, progressLabel } = args;
|
|
29810
|
+
const { ctx, internalSessions, proposal, projectInfo, maxIterations, iterationTimeoutMs, totalTimeoutMs, toolCtx, progressLabel } = args;
|
|
29811
|
+
const pipelineStart = Date.now();
|
|
29481
29812
|
const sessionResp = await ctx.client.session.create({
|
|
29482
29813
|
body: {},
|
|
29483
29814
|
query: { directory: ctx.directory }
|
|
@@ -29487,12 +29818,21 @@ async function runImplementation(args) {
|
|
|
29487
29818
|
return { ok: false, error: "Failed to create implementation session" };
|
|
29488
29819
|
internalSessions.add(sessionID);
|
|
29489
29820
|
const initialPrompt = IMPLEMENT_PROMPT(proposal, projectInfo);
|
|
29821
|
+
let lastDiffStat = getFileChanges(ctx.directory);
|
|
29822
|
+
let staleCount = 0;
|
|
29490
29823
|
try {
|
|
29491
29824
|
for (let i = 0;i < maxIterations; i++) {
|
|
29825
|
+
const elapsed = Date.now() - pipelineStart;
|
|
29826
|
+
if (elapsed > totalTimeoutMs) {
|
|
29827
|
+
log(`evolve_exe: total timeout exceeded (${(elapsed / 1000).toFixed(0)}s)`);
|
|
29828
|
+
return { ok: false, error: `Total timeout (${(totalTimeoutMs / 1000).toFixed(0)}s) exceeded at iteration ${i + 1}` };
|
|
29829
|
+
}
|
|
29830
|
+
const remaining = totalTimeoutMs - elapsed;
|
|
29831
|
+
const effectiveTimeout = Math.min(iterationTimeoutMs, remaining);
|
|
29492
29832
|
toolCtx.metadata({
|
|
29493
|
-
title: `${progressLabel} #${proposal.index}: implementing [${i + 1}/${maxIterations}]`
|
|
29833
|
+
title: `${progressLabel} #${proposal.index}: implementing [${i + 1}/${maxIterations}] (${(elapsed / 1000).toFixed(0)}s)`
|
|
29494
29834
|
});
|
|
29495
|
-
const prompt = i === 0 ? initialPrompt : buildContinuationPrompt2(initialPrompt, i + 1);
|
|
29835
|
+
const prompt = i === 0 ? initialPrompt : buildContinuationPrompt2(initialPrompt, i + 1, maxIterations);
|
|
29496
29836
|
try {
|
|
29497
29837
|
await withTimeout3(ctx.client.session.prompt({
|
|
29498
29838
|
path: { id: sessionID },
|
|
@@ -29501,11 +29841,16 @@ async function runImplementation(args) {
|
|
|
29501
29841
|
agent: "hephaestus"
|
|
29502
29842
|
},
|
|
29503
29843
|
query: { directory: ctx.directory }
|
|
29504
|
-
}),
|
|
29844
|
+
}), effectiveTimeout, `evolve_exe implementation iteration ${i + 1}`);
|
|
29505
29845
|
} catch (iterError) {
|
|
29506
29846
|
const msg = iterError instanceof Error ? iterError.message : String(iterError);
|
|
29507
29847
|
if (msg.startsWith("Timeout:")) {
|
|
29508
|
-
|
|
29848
|
+
const currentDiff2 = getFileChanges(ctx.directory);
|
|
29849
|
+
if (currentDiff2 && currentDiff2 !== lastDiffStat) {
|
|
29850
|
+
log(`evolve_exe: iteration ${i + 1} timed out but has file changes \u2014 treating as complete`);
|
|
29851
|
+
return { ok: true };
|
|
29852
|
+
}
|
|
29853
|
+
log(`evolve_exe: implementation iteration ${i + 1} timed out with no progress`);
|
|
29509
29854
|
return { ok: false, error: `Implementation timed out at iteration ${i + 1}` };
|
|
29510
29855
|
}
|
|
29511
29856
|
throw iterError;
|
|
@@ -29518,13 +29863,29 @@ async function runImplementation(args) {
|
|
|
29518
29863
|
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
29519
29864
|
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
29520
29865
|
`) ?? "";
|
|
29521
|
-
if (rawResult
|
|
29866
|
+
if (isCompletionDetected(rawResult)) {
|
|
29522
29867
|
log(`evolve_exe: implementation completed at iteration ${i + 1}`);
|
|
29523
29868
|
return { ok: true };
|
|
29524
29869
|
}
|
|
29525
|
-
|
|
29870
|
+
const currentDiff = getFileChanges(ctx.directory);
|
|
29871
|
+
if (currentDiff === lastDiffStat) {
|
|
29872
|
+
staleCount++;
|
|
29873
|
+
log(`evolve_exe: no file changes after iteration ${i + 1} (stale=${staleCount})`);
|
|
29874
|
+
if (staleCount >= 2) {
|
|
29875
|
+
return { ok: false, error: `No progress after ${staleCount} consecutive iterations \u2014 aborting` };
|
|
29876
|
+
}
|
|
29877
|
+
} else {
|
|
29878
|
+
staleCount = 0;
|
|
29879
|
+
lastDiffStat = currentDiff;
|
|
29880
|
+
}
|
|
29881
|
+
log(`evolve_exe: implementation iteration ${i + 1}/${maxIterations} \u2014 continuing`);
|
|
29882
|
+
}
|
|
29883
|
+
const finalDiff = getFileChanges(ctx.directory);
|
|
29884
|
+
if (finalDiff) {
|
|
29885
|
+
log(`evolve_exe: max iterations reached but files changed \u2014 treating as complete`);
|
|
29886
|
+
return { ok: true };
|
|
29526
29887
|
}
|
|
29527
|
-
return { ok: false, error: `Max iterations (${maxIterations}) reached without completion` };
|
|
29888
|
+
return { ok: false, error: `Max iterations (${maxIterations}) reached without completion or file changes` };
|
|
29528
29889
|
} finally {
|
|
29529
29890
|
internalSessions.delete(sessionID);
|
|
29530
29891
|
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
@@ -29677,6 +30038,300 @@ Git working directory is not clean. Commit or stash changes before publishing.
|
|
|
29677
30038
|
});
|
|
29678
30039
|
}
|
|
29679
30040
|
|
|
30041
|
+
// src/tools/health-check.ts
|
|
30042
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
30043
|
+
import * as fs11 from "fs";
|
|
30044
|
+
import * as path11 from "path";
|
|
30045
|
+
|
|
30046
|
+
// src/mcp/servers.ts
|
|
30047
|
+
var BUILTIN_MCPS = {
|
|
30048
|
+
context7: {
|
|
30049
|
+
command: "npx",
|
|
30050
|
+
args: ["-y", "@upstash/context7-mcp@latest"]
|
|
30051
|
+
}
|
|
30052
|
+
};
|
|
30053
|
+
|
|
30054
|
+
// src/mcp/index.ts
|
|
30055
|
+
async function registerMcps(ctx, disabled, apiKeys) {
|
|
30056
|
+
const disabledSet = new Set(disabled);
|
|
30057
|
+
const keys = apiKeys ?? {};
|
|
30058
|
+
for (const [name, config3] of Object.entries(BUILTIN_MCPS)) {
|
|
30059
|
+
if (disabledSet.has(name)) {
|
|
30060
|
+
log(`MCP ${name} disabled by config`);
|
|
30061
|
+
continue;
|
|
30062
|
+
}
|
|
30063
|
+
try {
|
|
30064
|
+
const args = [...config3.args];
|
|
30065
|
+
const envKey = `${name.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
30066
|
+
const apiKey = keys[name] ?? process.env[envKey];
|
|
30067
|
+
if (apiKey) {
|
|
30068
|
+
args.push("--api-key", apiKey);
|
|
30069
|
+
}
|
|
30070
|
+
const body = {
|
|
30071
|
+
name,
|
|
30072
|
+
command: config3.command,
|
|
30073
|
+
args
|
|
30074
|
+
};
|
|
30075
|
+
if (config3.env) {
|
|
30076
|
+
body.env = config3.env;
|
|
30077
|
+
}
|
|
30078
|
+
const client = ctx.client;
|
|
30079
|
+
await client.mcp?.add?.({ body });
|
|
30080
|
+
log(`MCP ${name} registered${apiKey ? " (with API key)" : ""}`);
|
|
30081
|
+
} catch (err) {
|
|
30082
|
+
log(`MCP ${name} registration failed: ${err}`);
|
|
30083
|
+
}
|
|
30084
|
+
}
|
|
30085
|
+
}
|
|
30086
|
+
|
|
30087
|
+
// src/tools/health-check.ts
|
|
30088
|
+
var BUILTIN_HOOK_IDS = [
|
|
30089
|
+
"keyword-detector",
|
|
30090
|
+
"rules-injector",
|
|
30091
|
+
"context-injector",
|
|
30092
|
+
"fragment-injector",
|
|
30093
|
+
"prompt-renderer",
|
|
30094
|
+
"todo-enforcer",
|
|
30095
|
+
"comment-checker",
|
|
30096
|
+
"token-truncation",
|
|
30097
|
+
"session-compaction"
|
|
30098
|
+
];
|
|
30099
|
+
var BUILTIN_TOOL_IDS = [
|
|
30100
|
+
"spawn_agent",
|
|
30101
|
+
"ralph_loop",
|
|
30102
|
+
"cancel_ralph",
|
|
30103
|
+
"batch_read",
|
|
30104
|
+
"ledger_save",
|
|
30105
|
+
"ledger_load",
|
|
30106
|
+
"ast_search",
|
|
30107
|
+
"evolve_apply",
|
|
30108
|
+
"evolve_scan",
|
|
30109
|
+
"evolve_score",
|
|
30110
|
+
"evolve_exe",
|
|
30111
|
+
"evolve_publish",
|
|
30112
|
+
"health_check"
|
|
30113
|
+
];
|
|
30114
|
+
var BinaryStatusSchema = exports_external.object({
|
|
30115
|
+
found: exports_external.boolean(),
|
|
30116
|
+
path: exports_external.string().optional(),
|
|
30117
|
+
version: exports_external.string().optional(),
|
|
30118
|
+
error: exports_external.string().optional()
|
|
30119
|
+
});
|
|
30120
|
+
var HealthCheckResultSchema = exports_external.object({
|
|
30121
|
+
ok: exports_external.boolean(),
|
|
30122
|
+
generatedAt: exports_external.string(),
|
|
30123
|
+
registry: exports_external.object({
|
|
30124
|
+
tools: exports_external.object({
|
|
30125
|
+
enabled: exports_external.array(exports_external.string()),
|
|
30126
|
+
disabled: exports_external.array(exports_external.string()),
|
|
30127
|
+
unknownDisabled: exports_external.array(exports_external.string())
|
|
30128
|
+
}),
|
|
30129
|
+
hooks: exports_external.object({
|
|
30130
|
+
enabled: exports_external.array(exports_external.string()),
|
|
30131
|
+
disabled: exports_external.array(exports_external.string()),
|
|
30132
|
+
unknownDisabled: exports_external.array(exports_external.string())
|
|
30133
|
+
}),
|
|
30134
|
+
mcps: exports_external.object({
|
|
30135
|
+
enabled: exports_external.array(exports_external.string()),
|
|
30136
|
+
disabled: exports_external.array(exports_external.string()),
|
|
30137
|
+
unknownDisabled: exports_external.array(exports_external.string())
|
|
30138
|
+
})
|
|
30139
|
+
}),
|
|
30140
|
+
loops: exports_external.object({
|
|
30141
|
+
ralph: exports_external.object({
|
|
30142
|
+
activeCount: exports_external.number(),
|
|
30143
|
+
active: exports_external.array(exports_external.object({
|
|
30144
|
+
sessionID: exports_external.string(),
|
|
30145
|
+
iteration: exports_external.number(),
|
|
30146
|
+
maxIterations: exports_external.number(),
|
|
30147
|
+
active: exports_external.boolean()
|
|
30148
|
+
}))
|
|
30149
|
+
})
|
|
30150
|
+
}),
|
|
30151
|
+
sessions: exports_external.object({
|
|
30152
|
+
internalSessionCount: exports_external.number()
|
|
30153
|
+
}),
|
|
30154
|
+
config: exports_external.object({
|
|
30155
|
+
astSearch: exports_external.object({
|
|
30156
|
+
enabled: exports_external.boolean(),
|
|
30157
|
+
binaryFound: exports_external.boolean(),
|
|
30158
|
+
binaryPath: exports_external.string().optional()
|
|
30159
|
+
}),
|
|
30160
|
+
mcp: exports_external.object({
|
|
30161
|
+
context7: exports_external.object({
|
|
30162
|
+
enabled: exports_external.boolean(),
|
|
30163
|
+
npxFound: exports_external.boolean(),
|
|
30164
|
+
npxPath: exports_external.string().optional()
|
|
30165
|
+
})
|
|
30166
|
+
})
|
|
30167
|
+
}),
|
|
30168
|
+
binaries: exports_external.record(exports_external.string(), BinaryStatusSchema),
|
|
30169
|
+
warnings: exports_external.array(exports_external.string())
|
|
30170
|
+
});
|
|
30171
|
+
function splitPathEnv() {
|
|
30172
|
+
const raw = process.env.PATH ?? "";
|
|
30173
|
+
const sep = path11.delimiter;
|
|
30174
|
+
return raw.split(sep).filter(Boolean);
|
|
30175
|
+
}
|
|
30176
|
+
function isExecutable(p) {
|
|
30177
|
+
try {
|
|
30178
|
+
const stat = fs11.statSync(p);
|
|
30179
|
+
if (!stat.isFile())
|
|
30180
|
+
return false;
|
|
30181
|
+
fs11.accessSync(p, fs11.constants.X_OK);
|
|
30182
|
+
return true;
|
|
30183
|
+
} catch {
|
|
30184
|
+
return false;
|
|
30185
|
+
}
|
|
30186
|
+
}
|
|
30187
|
+
function candidateNames(base) {
|
|
30188
|
+
if (process.platform !== "win32")
|
|
30189
|
+
return [base];
|
|
30190
|
+
return [base, `${base}.exe`, `${base}.cmd`, `${base}.bat`];
|
|
30191
|
+
}
|
|
30192
|
+
function findBinaryInPath(name) {
|
|
30193
|
+
const dirs = splitPathEnv();
|
|
30194
|
+
const candidates = candidateNames(name);
|
|
30195
|
+
for (const dir of dirs) {
|
|
30196
|
+
for (const c of candidates) {
|
|
30197
|
+
const full = path11.join(dir, c);
|
|
30198
|
+
if (isExecutable(full))
|
|
30199
|
+
return full;
|
|
30200
|
+
}
|
|
30201
|
+
}
|
|
30202
|
+
return null;
|
|
30203
|
+
}
|
|
30204
|
+
function getVersion(exePath, args) {
|
|
30205
|
+
try {
|
|
30206
|
+
const out = spawnSync4(exePath, args, { encoding: "utf-8" });
|
|
30207
|
+
const stdout = (out.stdout ?? "").toString().trim();
|
|
30208
|
+
const stderr = (out.stderr ?? "").toString().trim();
|
|
30209
|
+
if (out.status === 0) {
|
|
30210
|
+
const firstLine = (stdout || stderr).split(/\r?\n/)[0]?.trim();
|
|
30211
|
+
return firstLine ? { version: firstLine } : {};
|
|
30212
|
+
}
|
|
30213
|
+
const msg = stderr || stdout;
|
|
30214
|
+
return msg ? { error: msg.split(/\r?\n/)[0].trim() } : { error: `Exit code ${out.status ?? "unknown"}` };
|
|
30215
|
+
} catch (err) {
|
|
30216
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
30217
|
+
return { error: msg };
|
|
30218
|
+
}
|
|
30219
|
+
}
|
|
30220
|
+
function checkBinary(name, versionArgs = ["--version"], includeVersion = true) {
|
|
30221
|
+
const p = findBinaryInPath(name);
|
|
30222
|
+
if (!p)
|
|
30223
|
+
return { found: false };
|
|
30224
|
+
if (!includeVersion)
|
|
30225
|
+
return { found: true, path: p };
|
|
30226
|
+
const v = getVersion(p, versionArgs);
|
|
30227
|
+
return { found: true, path: p, version: v.version, error: v.error };
|
|
30228
|
+
}
|
|
30229
|
+
function uniqSorted(xs) {
|
|
30230
|
+
return [...new Set(xs)].sort((a, b) => a.localeCompare(b));
|
|
30231
|
+
}
|
|
30232
|
+
function computeUnknownDisabled(disabled, known) {
|
|
30233
|
+
const set3 = new Set(known);
|
|
30234
|
+
return uniqSorted(disabled.filter((x) => !set3.has(x)));
|
|
30235
|
+
}
|
|
30236
|
+
function enabledFromDisabled(known, disabled) {
|
|
30237
|
+
const dis = new Set(disabled);
|
|
30238
|
+
return uniqSorted(known.filter((x) => !dis.has(x)));
|
|
30239
|
+
}
|
|
30240
|
+
function createHealthCheckTool(ctx, deps) {
|
|
30241
|
+
return tool({
|
|
30242
|
+
description: "Collect plugin health diagnostics: registered hooks/tools, active loops, config consistency, and dependency binaries.",
|
|
30243
|
+
args: {
|
|
30244
|
+
includeVersions: tool.schema.boolean().optional().describe("If true, run each binary with --version (default: false)")
|
|
30245
|
+
},
|
|
30246
|
+
execute: async (args) => {
|
|
30247
|
+
const includeVersions = args.includeVersions ?? false;
|
|
30248
|
+
const disabledTools = deps.disabledTools ?? [];
|
|
30249
|
+
const disabledHooks = deps.disabledHooks ?? [];
|
|
30250
|
+
const disabledMcps = deps.disabledMcps ?? [];
|
|
30251
|
+
const knownTools = [...BUILTIN_TOOL_IDS];
|
|
30252
|
+
const knownHooks = [...BUILTIN_HOOK_IDS];
|
|
30253
|
+
const knownMcps = Object.keys(BUILTIN_MCPS);
|
|
30254
|
+
const enabledTools = uniqSorted(deps.toolNames);
|
|
30255
|
+
const enabledHooks = enabledFromDisabled(knownHooks, disabledHooks);
|
|
30256
|
+
const enabledMcps = enabledFromDisabled(knownMcps, disabledMcps);
|
|
30257
|
+
const unknownDisabledTools = computeUnknownDisabled(disabledTools, knownTools);
|
|
30258
|
+
const unknownDisabledHooks = computeUnknownDisabled(disabledHooks, knownHooks);
|
|
30259
|
+
const unknownDisabledMcps = computeUnknownDisabled(disabledMcps, knownMcps);
|
|
30260
|
+
const astPath = deps.astGrepBinary !== undefined ? deps.astGrepBinary : findAstGrepBinary();
|
|
30261
|
+
const astEnabled = !disabledTools.includes("ast_search");
|
|
30262
|
+
const astFound = Boolean(astPath);
|
|
30263
|
+
const binaries = {};
|
|
30264
|
+
binaries.node = checkBinary("node", ["--version"], includeVersions);
|
|
30265
|
+
binaries.bun = checkBinary("bun", ["--version"], includeVersions);
|
|
30266
|
+
binaries.npx = checkBinary("npx", ["--version"], includeVersions);
|
|
30267
|
+
binaries.git = checkBinary("git", ["--version"], includeVersions);
|
|
30268
|
+
binaries.gh = checkBinary("gh", ["--version"], includeVersions);
|
|
30269
|
+
binaries.rg = checkBinary("rg", ["--version"], includeVersions);
|
|
30270
|
+
if (astPath) {
|
|
30271
|
+
const v = includeVersions ? getVersion(astPath, ["--version"]) : {};
|
|
30272
|
+
binaries["ast-grep"] = { found: true, path: astPath, version: v.version, error: v.error };
|
|
30273
|
+
} else {
|
|
30274
|
+
const ast = checkBinary("sg", ["--version"], includeVersions);
|
|
30275
|
+
binaries["ast-grep"] = ast.found ? ast : { found: false };
|
|
30276
|
+
}
|
|
30277
|
+
const context7Enabled = enabledMcps.includes("context7");
|
|
30278
|
+
const npxFound = binaries.npx.found;
|
|
30279
|
+
const warnings = [];
|
|
30280
|
+
if (unknownDisabledTools.length > 0)
|
|
30281
|
+
warnings.push(`Unknown disabled tool id(s): ${unknownDisabledTools.join(", ")}`);
|
|
30282
|
+
if (unknownDisabledHooks.length > 0)
|
|
30283
|
+
warnings.push(`Unknown disabled hook id(s): ${unknownDisabledHooks.join(", ")}`);
|
|
30284
|
+
if (unknownDisabledMcps.length > 0)
|
|
30285
|
+
warnings.push(`Unknown disabled mcp id(s): ${unknownDisabledMcps.join(", ")}`);
|
|
30286
|
+
if (astEnabled && !astFound)
|
|
30287
|
+
warnings.push("ast_search is enabled but ast-grep binary was not found in PATH");
|
|
30288
|
+
if (context7Enabled && !npxFound)
|
|
30289
|
+
warnings.push("MCP 'context7' is enabled but 'npx' was not found in PATH");
|
|
30290
|
+
const active = getActiveRalphLoops();
|
|
30291
|
+
const result = {
|
|
30292
|
+
ok: warnings.length === 0,
|
|
30293
|
+
generatedAt: new Date().toISOString(),
|
|
30294
|
+
registry: {
|
|
30295
|
+
tools: { enabled: enabledTools, disabled: uniqSorted(disabledTools), unknownDisabled: unknownDisabledTools },
|
|
30296
|
+
hooks: { enabled: enabledHooks, disabled: uniqSorted(disabledHooks), unknownDisabled: unknownDisabledHooks },
|
|
30297
|
+
mcps: { enabled: enabledMcps, disabled: uniqSorted(disabledMcps), unknownDisabled: unknownDisabledMcps }
|
|
30298
|
+
},
|
|
30299
|
+
loops: {
|
|
30300
|
+
ralph: { activeCount: active.length, active }
|
|
30301
|
+
},
|
|
30302
|
+
sessions: {
|
|
30303
|
+
internalSessionCount: deps.internalSessions?.size ?? 0
|
|
30304
|
+
},
|
|
30305
|
+
config: {
|
|
30306
|
+
astSearch: {
|
|
30307
|
+
enabled: astEnabled,
|
|
30308
|
+
binaryFound: astFound,
|
|
30309
|
+
binaryPath: astPath ?? undefined
|
|
30310
|
+
},
|
|
30311
|
+
mcp: {
|
|
30312
|
+
context7: {
|
|
30313
|
+
enabled: context7Enabled,
|
|
30314
|
+
npxFound,
|
|
30315
|
+
npxPath: binaries.npx.path
|
|
30316
|
+
}
|
|
30317
|
+
}
|
|
30318
|
+
},
|
|
30319
|
+
binaries,
|
|
30320
|
+
warnings
|
|
30321
|
+
};
|
|
30322
|
+
const parsed = HealthCheckResultSchema.safeParse(result);
|
|
30323
|
+
if (!parsed.success) {
|
|
30324
|
+
return {
|
|
30325
|
+
ok: false,
|
|
30326
|
+
generatedAt: result.generatedAt,
|
|
30327
|
+
error: parsed.error.issues.map((i) => `${i.path.map(String).join(".")}: ${i.message}`).join("; ")
|
|
30328
|
+
};
|
|
30329
|
+
}
|
|
30330
|
+
return result;
|
|
30331
|
+
}
|
|
30332
|
+
});
|
|
30333
|
+
}
|
|
30334
|
+
|
|
29680
30335
|
// src/hooks/todo-enforcer.ts
|
|
29681
30336
|
var DEFAULT_MAX_ENFORCEMENTS = 5;
|
|
29682
30337
|
var sessionState = new Map;
|
|
@@ -29759,7 +30414,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
|
|
|
29759
30414
|
}
|
|
29760
30415
|
|
|
29761
30416
|
// src/hooks/comment-checker.ts
|
|
29762
|
-
import * as
|
|
30417
|
+
import * as fs12 from "fs";
|
|
29763
30418
|
var AI_SLOP_PATTERNS = [
|
|
29764
30419
|
/\/\/ .{80,}/,
|
|
29765
30420
|
/\/\/ (This|The|We|Here|Note:)/i,
|
|
@@ -29838,7 +30493,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
|
|
|
29838
30493
|
if (!filePath || !isCodeFile(filePath))
|
|
29839
30494
|
return;
|
|
29840
30495
|
try {
|
|
29841
|
-
const content =
|
|
30496
|
+
const content = fs12.readFileSync(filePath, "utf-8");
|
|
29842
30497
|
const result = checkComments(content, filePath, maxRatio, slopThreshold);
|
|
29843
30498
|
if (result.shouldWarn) {
|
|
29844
30499
|
output.output += `
|
|
@@ -29997,127 +30652,6 @@ function createSessionCompactionHook(ctx, internalSessions) {
|
|
|
29997
30652
|
};
|
|
29998
30653
|
}
|
|
29999
30654
|
|
|
30000
|
-
// src/concurrency/semaphore.ts
|
|
30001
|
-
class Semaphore {
|
|
30002
|
-
max;
|
|
30003
|
-
queue = [];
|
|
30004
|
-
active = 0;
|
|
30005
|
-
constructor(max) {
|
|
30006
|
-
this.max = max;
|
|
30007
|
-
}
|
|
30008
|
-
async acquire() {
|
|
30009
|
-
if (this.active < this.max) {
|
|
30010
|
-
this.active++;
|
|
30011
|
-
return;
|
|
30012
|
-
}
|
|
30013
|
-
return new Promise((resolve2) => {
|
|
30014
|
-
this.queue.push(() => {
|
|
30015
|
-
this.active++;
|
|
30016
|
-
resolve2();
|
|
30017
|
-
});
|
|
30018
|
-
});
|
|
30019
|
-
}
|
|
30020
|
-
release() {
|
|
30021
|
-
if (this.active <= 0)
|
|
30022
|
-
return;
|
|
30023
|
-
this.active--;
|
|
30024
|
-
const next = this.queue.shift();
|
|
30025
|
-
if (next)
|
|
30026
|
-
next();
|
|
30027
|
-
}
|
|
30028
|
-
get pending() {
|
|
30029
|
-
return this.queue.length;
|
|
30030
|
-
}
|
|
30031
|
-
get running() {
|
|
30032
|
-
return this.active;
|
|
30033
|
-
}
|
|
30034
|
-
}
|
|
30035
|
-
// src/concurrency/pool.ts
|
|
30036
|
-
function extractProvider(model) {
|
|
30037
|
-
const slash = model.indexOf("/");
|
|
30038
|
-
return slash > 0 ? model.slice(0, slash) : model;
|
|
30039
|
-
}
|
|
30040
|
-
|
|
30041
|
-
class ConcurrencyPool {
|
|
30042
|
-
global;
|
|
30043
|
-
providers = new Map;
|
|
30044
|
-
models = new Map;
|
|
30045
|
-
config;
|
|
30046
|
-
constructor(config3) {
|
|
30047
|
-
this.config = config3;
|
|
30048
|
-
this.global = new Semaphore(config3.defaultConcurrency ?? Infinity);
|
|
30049
|
-
for (const [provider, limit] of Object.entries(config3.providerConcurrency ?? {})) {
|
|
30050
|
-
this.providers.set(provider, new Semaphore(limit));
|
|
30051
|
-
}
|
|
30052
|
-
for (const [model, limit] of Object.entries(config3.modelConcurrency ?? {})) {
|
|
30053
|
-
this.models.set(model, new Semaphore(limit));
|
|
30054
|
-
}
|
|
30055
|
-
}
|
|
30056
|
-
async run(model, fn) {
|
|
30057
|
-
const provider = extractProvider(model);
|
|
30058
|
-
const semaphores = [];
|
|
30059
|
-
const modelSem = this.models.get(model);
|
|
30060
|
-
if (modelSem) {
|
|
30061
|
-
await modelSem.acquire();
|
|
30062
|
-
semaphores.push(modelSem);
|
|
30063
|
-
}
|
|
30064
|
-
const providerSem = this.providers.get(provider);
|
|
30065
|
-
if (providerSem) {
|
|
30066
|
-
await providerSem.acquire();
|
|
30067
|
-
semaphores.push(providerSem);
|
|
30068
|
-
}
|
|
30069
|
-
await this.global.acquire();
|
|
30070
|
-
semaphores.push(this.global);
|
|
30071
|
-
try {
|
|
30072
|
-
return await fn();
|
|
30073
|
-
} finally {
|
|
30074
|
-
for (let i = semaphores.length - 1;i >= 0; i--) {
|
|
30075
|
-
semaphores[i].release();
|
|
30076
|
-
}
|
|
30077
|
-
}
|
|
30078
|
-
}
|
|
30079
|
-
}
|
|
30080
|
-
// src/mcp/servers.ts
|
|
30081
|
-
var BUILTIN_MCPS = {
|
|
30082
|
-
context7: {
|
|
30083
|
-
command: "npx",
|
|
30084
|
-
args: ["-y", "@upstash/context7-mcp@latest"]
|
|
30085
|
-
}
|
|
30086
|
-
};
|
|
30087
|
-
|
|
30088
|
-
// src/mcp/index.ts
|
|
30089
|
-
async function registerMcps(ctx, disabled, apiKeys) {
|
|
30090
|
-
const disabledSet = new Set(disabled);
|
|
30091
|
-
const keys = apiKeys ?? {};
|
|
30092
|
-
for (const [name, config3] of Object.entries(BUILTIN_MCPS)) {
|
|
30093
|
-
if (disabledSet.has(name)) {
|
|
30094
|
-
log(`MCP ${name} disabled by config`);
|
|
30095
|
-
continue;
|
|
30096
|
-
}
|
|
30097
|
-
try {
|
|
30098
|
-
const args = [...config3.args];
|
|
30099
|
-
const envKey = `${name.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
30100
|
-
const apiKey = keys[name] ?? process.env[envKey];
|
|
30101
|
-
if (apiKey) {
|
|
30102
|
-
args.push("--api-key", apiKey);
|
|
30103
|
-
}
|
|
30104
|
-
const body = {
|
|
30105
|
-
name,
|
|
30106
|
-
command: config3.command,
|
|
30107
|
-
args
|
|
30108
|
-
};
|
|
30109
|
-
if (config3.env) {
|
|
30110
|
-
body.env = config3.env;
|
|
30111
|
-
}
|
|
30112
|
-
const client = ctx.client;
|
|
30113
|
-
await client.mcp?.add?.({ body });
|
|
30114
|
-
log(`MCP ${name} registered${apiKey ? " (with API key)" : ""}`);
|
|
30115
|
-
} catch (err) {
|
|
30116
|
-
log(`MCP ${name} registration failed: ${err}`);
|
|
30117
|
-
}
|
|
30118
|
-
}
|
|
30119
|
-
}
|
|
30120
|
-
|
|
30121
30655
|
// src/index.ts
|
|
30122
30656
|
var OpenCodeUltra = async (ctx) => {
|
|
30123
30657
|
log("ENTRY \u2014 plugin loading", { directory: ctx.directory });
|
|
@@ -30227,6 +30761,16 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
30227
30761
|
if (!disabledTools.has("evolve_publish")) {
|
|
30228
30762
|
toolRegistry.evolve_publish = createEvolvePublishTool();
|
|
30229
30763
|
}
|
|
30764
|
+
if (!disabledTools.has("health_check")) {
|
|
30765
|
+
toolRegistry.health_check = createHealthCheckTool(ctx, {
|
|
30766
|
+
toolNames: Object.keys(toolRegistry),
|
|
30767
|
+
disabledTools: [...disabledTools],
|
|
30768
|
+
disabledHooks: [...pluginConfig.disabled_hooks ?? []],
|
|
30769
|
+
disabledMcps: [...pluginConfig.disabled_mcps ?? []],
|
|
30770
|
+
internalSessions,
|
|
30771
|
+
astGrepBinary: findAstGrepBinary()
|
|
30772
|
+
});
|
|
30773
|
+
}
|
|
30230
30774
|
return {
|
|
30231
30775
|
tool: toolRegistry,
|
|
30232
30776
|
config: async (config3) => {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export interface BackoffOptions {
|
|
2
|
+
/** Base delay in ms (default: 200) */
|
|
3
|
+
baseDelayMs?: number;
|
|
4
|
+
/** Max delay in ms (default: 10_000) */
|
|
5
|
+
maxDelayMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export type BackoffJitterMode = "full" | "none";
|
|
8
|
+
export interface ExponentialBackoffOptions extends BackoffOptions {
|
|
9
|
+
/** Jitter strategy (default: full) */
|
|
10
|
+
jitter?: BackoffJitterMode;
|
|
11
|
+
/** Random generator in [0, 1) (default: Math.random) */
|
|
12
|
+
rng?: () => number;
|
|
13
|
+
}
|
|
14
|
+
export declare function computeExponentialBackoffDelayMs(attempt: number, opts?: ExponentialBackoffOptions): number;
|
|
15
|
+
export interface CircuitBreakerOptions {
|
|
16
|
+
/** Open circuit after this many consecutive retryable failures (default: 3) */
|
|
17
|
+
failureThreshold?: number;
|
|
18
|
+
/** How long the circuit stays open before allowing a single trial (default: 10_000) */
|
|
19
|
+
cooldownMs?: number;
|
|
20
|
+
}
|
|
21
|
+
type CircuitState = "closed" | "open" | "half_open";
|
|
22
|
+
export declare class CircuitBreakerOpenError extends Error {
|
|
23
|
+
readonly provider: string;
|
|
24
|
+
readonly waitMs: number;
|
|
25
|
+
constructor(provider: string, waitMs: number);
|
|
26
|
+
}
|
|
27
|
+
export declare class CircuitBreaker {
|
|
28
|
+
private state;
|
|
29
|
+
private consecutiveFailures;
|
|
30
|
+
private openUntil;
|
|
31
|
+
private halfOpenInFlight;
|
|
32
|
+
private failureThreshold;
|
|
33
|
+
private cooldownMs;
|
|
34
|
+
constructor(opts?: CircuitBreakerOptions);
|
|
35
|
+
snapshot(now: number): {
|
|
36
|
+
state: CircuitState;
|
|
37
|
+
consecutiveFailures: number;
|
|
38
|
+
openForMs: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Gate a request. If open, returns not allowed with waitMs.
|
|
42
|
+
* If half-open, only allows a single in-flight probe.
|
|
43
|
+
*/
|
|
44
|
+
enter(now: number): {
|
|
45
|
+
allowed: boolean;
|
|
46
|
+
waitMs?: number;
|
|
47
|
+
};
|
|
48
|
+
onSuccess(): void;
|
|
49
|
+
onFailure(now: number): void;
|
|
50
|
+
}
|
|
51
|
+
export interface RetryBudgetOptions {
|
|
52
|
+
/** Max retries per provider per interval (default: 20) */
|
|
53
|
+
maxRetries?: number;
|
|
54
|
+
/** Interval for budget reset (default: 60_000) */
|
|
55
|
+
intervalMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare class RetryBudgetExceededError extends Error {
|
|
58
|
+
readonly provider: string;
|
|
59
|
+
constructor(provider: string);
|
|
60
|
+
}
|
|
61
|
+
export declare class RetryBudget {
|
|
62
|
+
private maxRetries;
|
|
63
|
+
private intervalMs;
|
|
64
|
+
private state;
|
|
65
|
+
constructor(opts?: RetryBudgetOptions);
|
|
66
|
+
trySpend(provider: string, cost?: number): boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface RetryDecision {
|
|
69
|
+
retryable: boolean;
|
|
70
|
+
/** HTTP status code when available */
|
|
71
|
+
status?: number;
|
|
72
|
+
/** Retry-After when available */
|
|
73
|
+
retryAfterMs?: number;
|
|
74
|
+
}
|
|
75
|
+
export declare function parseRetryAfterMs(value: string, now: number): number | undefined;
|
|
76
|
+
export declare function classifyRetryableError(err: unknown, now?: number): RetryDecision;
|
|
77
|
+
export interface AdaptiveRetryOptions {
|
|
78
|
+
/** Max total attempts including the first attempt (default: 4) */
|
|
79
|
+
maxAttempts?: number;
|
|
80
|
+
/** Backoff options (default: base 200, max 10_000) */
|
|
81
|
+
backoff?: ExponentialBackoffOptions;
|
|
82
|
+
/** Cap any retry-after delays to this max (default: 30_000) */
|
|
83
|
+
maxRetryAfterMs?: number;
|
|
84
|
+
/** Treat this classifier result as authoritative (default: classifyRetryableError) */
|
|
85
|
+
classify?: (err: unknown, now: number) => RetryDecision;
|
|
86
|
+
/** Sleep function used for delays (default: setTimeout) */
|
|
87
|
+
sleep?: (ms: number) => Promise<void>;
|
|
88
|
+
/** Time source (default: Date.now) */
|
|
89
|
+
now?: () => number;
|
|
90
|
+
}
|
|
91
|
+
export declare function adaptiveRetry<T>(provider: string, fn: () => Promise<T>, breaker: CircuitBreaker, budget: RetryBudget, opts?: AdaptiveRetryOptions): Promise<T>;
|
|
92
|
+
export interface ProviderResilienceOptions {
|
|
93
|
+
breaker?: CircuitBreakerOptions;
|
|
94
|
+
retryBudget?: RetryBudgetOptions;
|
|
95
|
+
retry?: AdaptiveRetryOptions;
|
|
96
|
+
}
|
|
97
|
+
export declare class ProviderResilience {
|
|
98
|
+
private breakers;
|
|
99
|
+
private budgets;
|
|
100
|
+
private opts;
|
|
101
|
+
constructor(opts?: ProviderResilienceOptions);
|
|
102
|
+
private getBreaker;
|
|
103
|
+
private getBudget;
|
|
104
|
+
run<T>(provider: string, fn: () => Promise<T>, retryOverrides?: AdaptiveRetryOptions): Promise<T>;
|
|
105
|
+
snapshot(provider: string, now?: number): {
|
|
106
|
+
breaker: ReturnType<CircuitBreaker["snapshot"]>;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export {};
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export { parseJsonc } from "./jsonc";
|
|
|
3
3
|
export { log } from "./log";
|
|
4
4
|
export type { ExtendedClient, ModelRef, ToastOptions, SystemTransformInput } from "./types";
|
|
5
5
|
export { TtlMap } from "./ttl-map";
|
|
6
|
+
export { CircuitBreaker, CircuitBreakerOpenError, ProviderResilience, RetryBudget, RetryBudgetExceededError, adaptiveRetry, classifyRetryableError, computeExponentialBackoffDelayMs, parseRetryAfterMs, } from "./concurrency";
|
|
7
|
+
export type { AdaptiveRetryOptions, BackoffJitterMode, BackoffOptions, CircuitBreakerOptions, ExponentialBackoffOptions, ProviderResilienceOptions, RetryBudgetOptions, RetryDecision, } from "./concurrency";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { type ActiveRalphLoopInfo } from "./ralph-loop";
|
|
5
|
+
export declare const BUILTIN_HOOK_IDS: readonly ["keyword-detector", "rules-injector", "context-injector", "fragment-injector", "prompt-renderer", "todo-enforcer", "comment-checker", "token-truncation", "session-compaction"];
|
|
6
|
+
export declare const BUILTIN_TOOL_IDS: readonly ["spawn_agent", "ralph_loop", "cancel_ralph", "batch_read", "ledger_save", "ledger_load", "ast_search", "evolve_apply", "evolve_scan", "evolve_score", "evolve_exe", "evolve_publish", "health_check"];
|
|
7
|
+
export type BuiltinHookId = (typeof BUILTIN_HOOK_IDS)[number];
|
|
8
|
+
export type BuiltinToolId = (typeof BUILTIN_TOOL_IDS)[number];
|
|
9
|
+
export interface BinaryStatus {
|
|
10
|
+
found: boolean;
|
|
11
|
+
path?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HealthCheckResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
generatedAt: string;
|
|
18
|
+
registry: {
|
|
19
|
+
tools: {
|
|
20
|
+
enabled: string[];
|
|
21
|
+
disabled: string[];
|
|
22
|
+
unknownDisabled: string[];
|
|
23
|
+
};
|
|
24
|
+
hooks: {
|
|
25
|
+
enabled: string[];
|
|
26
|
+
disabled: string[];
|
|
27
|
+
unknownDisabled: string[];
|
|
28
|
+
};
|
|
29
|
+
mcps: {
|
|
30
|
+
enabled: string[];
|
|
31
|
+
disabled: string[];
|
|
32
|
+
unknownDisabled: string[];
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
loops: {
|
|
36
|
+
ralph: {
|
|
37
|
+
activeCount: number;
|
|
38
|
+
active: ActiveRalphLoopInfo[];
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
sessions: {
|
|
42
|
+
internalSessionCount: number;
|
|
43
|
+
};
|
|
44
|
+
config: {
|
|
45
|
+
astSearch: {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
binaryFound: boolean;
|
|
48
|
+
binaryPath?: string;
|
|
49
|
+
};
|
|
50
|
+
mcp: {
|
|
51
|
+
context7: {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
npxFound: boolean;
|
|
54
|
+
npxPath?: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
binaries: Record<string, BinaryStatus>;
|
|
59
|
+
warnings: string[];
|
|
60
|
+
}
|
|
61
|
+
export declare const BinaryStatusSchema: z.ZodType<BinaryStatus>;
|
|
62
|
+
export declare const HealthCheckResultSchema: z.ZodType<HealthCheckResult>;
|
|
63
|
+
export interface HealthCheckDeps {
|
|
64
|
+
toolNames: string[];
|
|
65
|
+
disabledTools?: string[];
|
|
66
|
+
disabledHooks?: string[];
|
|
67
|
+
disabledMcps?: string[];
|
|
68
|
+
internalSessions?: Set<string>;
|
|
69
|
+
astGrepBinary?: string | null;
|
|
70
|
+
}
|
|
71
|
+
export declare function findBinaryInPath(name: string): string | null;
|
|
72
|
+
export declare function checkBinary(name: string, versionArgs?: string[], includeVersion?: boolean): BinaryStatus;
|
|
73
|
+
export declare function createHealthCheckTool(ctx: PluginInput, deps: HealthCheckDeps): ReturnType<typeof tool>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
export interface ActiveRalphLoopInfo {
|
|
3
|
+
sessionID: string;
|
|
4
|
+
iteration: number;
|
|
5
|
+
maxIterations: number;
|
|
6
|
+
active: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function getActiveRalphLoops(): ActiveRalphLoopInfo[];
|
|
9
|
+
export interface RalphLoopDeps {
|
|
10
|
+
/** Per-iteration timeout in ms (default: 180000 = 3min) */
|
|
11
|
+
iterationTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function createRalphLoopTools(ctx: PluginInput, internalSessions: Set<string>, deps?: RalphLoopDeps): {
|
|
14
|
+
ralph_loop: any;
|
|
15
|
+
cancel_ralph: any;
|
|
16
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { ProviderResilience } from "../shared";
|
|
4
|
+
import { type ConcurrencyPool } from "../concurrency";
|
|
5
|
+
import type { CategoryConfig } from "../categories";
|
|
6
|
+
export interface SpawnAgentDeps {
|
|
7
|
+
pool?: ConcurrencyPool;
|
|
8
|
+
categories?: Record<string, CategoryConfig>;
|
|
9
|
+
resolveAgentModel?: (agent: string) => string | undefined;
|
|
10
|
+
/** Provider-level retry/backoff + circuit breaker */
|
|
11
|
+
resilience?: ProviderResilience;
|
|
12
|
+
/** Max total concurrent spawned sessions (default: 15) */
|
|
13
|
+
maxTotalSpawned?: number;
|
|
14
|
+
/** Per-agent timeout in ms (default: 180000 = 3min) */
|
|
15
|
+
agentTimeoutMs?: number;
|
|
16
|
+
/** Reference to internalSessions for spawn limit check */
|
|
17
|
+
internalSessions?: Set<string>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createSpawnAgentTool(ctx: PluginInput, internalSessions: Set<string>, deps?: SpawnAgentDeps): ReturnType<typeof tool>;
|