@coool/pi-adaptive 1.0.0 → 1.0.1
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/README.md +98 -196
- package/dist/index.d.ts +2 -1
- package/dist/index.js +603 -343
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -693,8 +693,13 @@ function updatePreferences(file, patch) {
|
|
|
693
693
|
});
|
|
694
694
|
}
|
|
695
695
|
// ../core/src/state/receipts/bound.ts
|
|
696
|
+
/**
|
|
697
|
+
* Agent-written evidence can carry emoji; cutting between the halves of a
|
|
698
|
+
* surrogate pair would persist a lone surrogate that renders as garbage and
|
|
699
|
+
* throws in URI encoders, so the cut backs off to the pair boundary.
|
|
700
|
+
*/
|
|
696
701
|
function boundText(raw) {
|
|
697
|
-
return raw
|
|
702
|
+
return truncateBounded(raw, 128);
|
|
698
703
|
}
|
|
699
704
|
function boundRef(ref) {
|
|
700
705
|
return {
|
|
@@ -804,8 +809,7 @@ function isRecord$2(value) {
|
|
|
804
809
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
805
810
|
}
|
|
806
811
|
function boundString(raw) {
|
|
807
|
-
|
|
808
|
-
return raw.length > 128 ? raw.slice(0, 128) : raw;
|
|
812
|
+
return typeof raw === "string" ? boundText(raw) : void 0;
|
|
809
813
|
}
|
|
810
814
|
function isFiniteNonNegative(raw) {
|
|
811
815
|
return typeof raw === "number" && Number.isFinite(raw) && raw >= 0;
|
|
@@ -1650,6 +1654,15 @@ function classifyFailure(evidence) {
|
|
|
1650
1654
|
* refuses. Termination rests on the budget, which the caller must carry
|
|
1651
1655
|
* across every unit of one task (each escalation continues on a new unit).
|
|
1652
1656
|
*/
|
|
1657
|
+
/**
|
|
1658
|
+
* Only a well-formed whole-number budget can bound the loop: NaN, infinite,
|
|
1659
|
+
* fractional, or negative counters would make `used >= max` unreachable or
|
|
1660
|
+
* leave a negative remainder, so they read as exhausted (finite is the
|
|
1661
|
+
* invariant, not the caller's good behavior).
|
|
1662
|
+
*/
|
|
1663
|
+
function hasBudgetLeft({ used, max }) {
|
|
1664
|
+
return Number.isSafeInteger(used) && Number.isSafeInteger(max) && used >= 0 && used < max;
|
|
1665
|
+
}
|
|
1653
1666
|
function escalate(input, target, step, tier) {
|
|
1654
1667
|
return {
|
|
1655
1668
|
kind: "escalate",
|
|
@@ -1669,7 +1682,7 @@ function planEscalation(input) {
|
|
|
1669
1682
|
kind: "defer",
|
|
1670
1683
|
reason: "escalation-not-justified"
|
|
1671
1684
|
};
|
|
1672
|
-
if (input.budget
|
|
1685
|
+
if (!hasBudgetLeft(input.budget)) return {
|
|
1673
1686
|
kind: "defer",
|
|
1674
1687
|
reason: "escalation-budget-exhausted"
|
|
1675
1688
|
};
|
|
@@ -2811,6 +2824,7 @@ var ResourceHealthRegistry = class {
|
|
|
2811
2824
|
const records = resourceChain(key).map((chainKey) => this.#sweptRecord(chainKey, now));
|
|
2812
2825
|
const hard = hardest(records, now);
|
|
2813
2826
|
if (hard !== void 0) return blocked(hard.reason, hard.key, hard.retryAtMs);
|
|
2827
|
+
const due = [];
|
|
2814
2828
|
for (const record of records) {
|
|
2815
2829
|
if (record === void 0) continue;
|
|
2816
2830
|
const expired = expireProbe(record.breaker, now, this.#config.breaker);
|
|
@@ -2821,14 +2835,20 @@ var ResourceHealthRegistry = class {
|
|
|
2821
2835
|
};
|
|
2822
2836
|
if (current !== record) this.#records.set(resourceKeyID(record.key), current);
|
|
2823
2837
|
const admission = admitProbe(current.breaker, now);
|
|
2824
|
-
if (admission.kind === "closed") continue;
|
|
2825
2838
|
if (admission.kind === "waiting") return blocked("breaker-open", record.key, admission.retryAtMs);
|
|
2826
2839
|
if (admission.kind === "in-flight") return blocked("probe-in-flight", record.key, void 0);
|
|
2840
|
+
if (admission.kind === "admitted") due.push({
|
|
2841
|
+
record,
|
|
2842
|
+
breaker: admission.record
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
const first = due[0];
|
|
2846
|
+
if (first !== void 0) {
|
|
2827
2847
|
const probeId = this.#newProbeId();
|
|
2828
|
-
this.#records.set(resourceKeyID(record.key), {
|
|
2829
|
-
...record,
|
|
2848
|
+
this.#records.set(resourceKeyID(first.record.key), {
|
|
2849
|
+
...first.record,
|
|
2830
2850
|
breaker: {
|
|
2831
|
-
...
|
|
2851
|
+
...first.breaker,
|
|
2832
2852
|
probeId,
|
|
2833
2853
|
probeAt: now
|
|
2834
2854
|
},
|
|
@@ -2838,7 +2858,7 @@ var ResourceHealthRegistry = class {
|
|
|
2838
2858
|
kind: "probe",
|
|
2839
2859
|
ticket: {
|
|
2840
2860
|
id: probeId,
|
|
2841
|
-
key: record.key
|
|
2861
|
+
key: first.record.key
|
|
2842
2862
|
}
|
|
2843
2863
|
};
|
|
2844
2864
|
}
|
|
@@ -3611,7 +3631,7 @@ function planWake(targets, now, fruitless) {
|
|
|
3611
3631
|
* waiting for a stated reset is free, only resumed work that failed again
|
|
3612
3632
|
* counts.
|
|
3613
3633
|
*/
|
|
3614
|
-
const MAX_WAKES =
|
|
3634
|
+
const MAX_WAKES = 96;
|
|
3615
3635
|
const PARK_ENTRY = "adaptive.parked";
|
|
3616
3636
|
var WaitCancelled = class extends Error {
|
|
3617
3637
|
constructor() {
|
|
@@ -3687,14 +3707,18 @@ function claimPark(file, record, now, manual = false) {
|
|
|
3687
3707
|
};
|
|
3688
3708
|
});
|
|
3689
3709
|
}
|
|
3710
|
+
/** Drops the session's records; true when one was still waiting (not yet claimed). */
|
|
3690
3711
|
function cancelPark(file, session) {
|
|
3691
|
-
return file.update((value) =>
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3712
|
+
return file.update((value) => {
|
|
3713
|
+
const rows = records(value);
|
|
3714
|
+
return {
|
|
3715
|
+
next: {
|
|
3716
|
+
version: 1,
|
|
3717
|
+
records: rows.filter((row) => row.session !== session)
|
|
3718
|
+
},
|
|
3719
|
+
result: rows.some((row) => row.session === session && !row.claimed)
|
|
3720
|
+
};
|
|
3721
|
+
});
|
|
3698
3722
|
}
|
|
3699
3723
|
/** Abortable ticks, shared by countdowns and long owned headless waits. */
|
|
3700
3724
|
async function waitUntil(sources, due, signal, tick) {
|
|
@@ -4018,7 +4042,10 @@ function virtualModels(sources, activeTools, parking, dispatched, feedback) {
|
|
|
4018
4042
|
}
|
|
4019
4043
|
selectedRef = fallback;
|
|
4020
4044
|
} else {
|
|
4021
|
-
if (decision.kind !== "apply")
|
|
4045
|
+
if (decision.kind !== "apply") {
|
|
4046
|
+
const undeclared = decision.reason === "no-compatible-resource" && decision.detail?.includes("tool-use-unsupported") === true;
|
|
4047
|
+
throw new Error(undeclared ? `Adaptive unavailable: ${decision.reason}. Run /adaptive tools to declare which of your models call tools.` : `Adaptive unavailable: ${decision.reason}. Check your tier settings.`);
|
|
4048
|
+
}
|
|
4022
4049
|
selectedRef = decision.model;
|
|
4023
4050
|
}
|
|
4024
4051
|
const selected = prepared.models.find((model) => model.provider === selectedRef.providerID && model.id === selectedRef.id);
|
|
@@ -4298,11 +4325,12 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
|
|
|
4298
4325
|
disarm?.();
|
|
4299
4326
|
disarm = void 0;
|
|
4300
4327
|
};
|
|
4328
|
+
/** True when a still-waiting record was dropped. */
|
|
4301
4329
|
const cancel = async (ctx) => {
|
|
4302
4330
|
stop();
|
|
4303
4331
|
clearParked(ctx);
|
|
4304
4332
|
const session = ctx.sessionManager.getSessionId?.();
|
|
4305
|
-
|
|
4333
|
+
return session !== void 0 && ctx.sessionManager.getBranch().some((entry) => entry.customType === "adaptive.parked") && cancelPark(sources.parking.file, session);
|
|
4306
4334
|
};
|
|
4307
4335
|
const noteResume = async (owned, tier) => {
|
|
4308
4336
|
try {
|
|
@@ -4320,9 +4348,9 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
|
|
|
4320
4348
|
const owned = generation;
|
|
4321
4349
|
if (!await claimPark(sources.parking.file, record, now, manual)) {
|
|
4322
4350
|
clearParked(ctx);
|
|
4323
|
-
return;
|
|
4351
|
+
return false;
|
|
4324
4352
|
}
|
|
4325
|
-
if (owned !== generation) return;
|
|
4353
|
+
if (owned !== generation) return false;
|
|
4326
4354
|
clearParked(ctx);
|
|
4327
4355
|
if (saved.input !== void 0) {
|
|
4328
4356
|
resumedInput = {
|
|
@@ -4351,6 +4379,7 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
|
|
|
4351
4379
|
}
|
|
4352
4380
|
}, { triggerTurn: true });
|
|
4353
4381
|
await noteResume(owned, record.tier);
|
|
4382
|
+
return true;
|
|
4354
4383
|
};
|
|
4355
4384
|
const arm = (ctx, record, saved) => {
|
|
4356
4385
|
showParked(ctx, record);
|
|
@@ -4517,12 +4546,9 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
|
|
|
4517
4546
|
const session = ctx.sessionManager.getSessionId?.();
|
|
4518
4547
|
const record = session === void 0 ? void 0 : await findPark(sources.parking.file, session);
|
|
4519
4548
|
const saved = record === void 0 ? void 0 : parkedPayload(ctx, record.token);
|
|
4520
|
-
if (record === void 0 || saved === void 0)
|
|
4521
|
-
ctx.ui?.notify("Nothing is parked in this session.", "info");
|
|
4522
|
-
return;
|
|
4523
|
-
}
|
|
4549
|
+
if (record === void 0 || saved === void 0) return "not-parked";
|
|
4524
4550
|
stop();
|
|
4525
|
-
await wake(ctx, record, saved, record.due, true);
|
|
4551
|
+
return await wake(ctx, record, saved, record.due, true) ? "resumed" : "not-parked";
|
|
4526
4552
|
};
|
|
4527
4553
|
const clearServed = async (task, ctx) => {
|
|
4528
4554
|
try {
|
|
@@ -4544,7 +4570,7 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
|
|
|
4544
4570
|
sessionStart,
|
|
4545
4571
|
resume,
|
|
4546
4572
|
clearServed,
|
|
4547
|
-
cancel,
|
|
4573
|
+
cancel: async (ctx) => await cancel(ctx) ? "cancelled" : "not-parked",
|
|
4548
4574
|
shutdown: stop,
|
|
4549
4575
|
async recover(recovery, ctx) {
|
|
4550
4576
|
const id = selectedVirtual(ctx.model);
|
|
@@ -4638,31 +4664,6 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
|
|
|
4638
4664
|
}
|
|
4639
4665
|
};
|
|
4640
4666
|
}
|
|
4641
|
-
// ../core/src/flow/waiting.ts
|
|
4642
|
-
const DAY_MS = 864e5;
|
|
4643
|
-
const WEEK_DAYS = 6;
|
|
4644
|
-
function startOfDay(at) {
|
|
4645
|
-
const date = new Date(at);
|
|
4646
|
-
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
4647
|
-
}
|
|
4648
|
-
function clock(date) {
|
|
4649
|
-
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
4650
|
-
}
|
|
4651
|
-
/**
|
|
4652
|
-
* Short human text for a parked session (ADP-024): "waiting · resumes 18:42". Weekly limits reset days away, so
|
|
4653
|
-
* a later day gains its weekday, and beyond a week its date. Local fields, not the locale formatter, keep the
|
|
4654
|
-
* text identical across hosts.
|
|
4655
|
-
*/
|
|
4656
|
-
function waitingLabel(resumeAt, now) {
|
|
4657
|
-
if (resumeAt === null) return "waiting";
|
|
4658
|
-
if (resumeAt <= now) return "waiting · resuming";
|
|
4659
|
-
const date = new Date(resumeAt);
|
|
4660
|
-
const days = Math.round((startOfDay(resumeAt) - startOfDay(now)) / DAY_MS);
|
|
4661
|
-
if (days === 0) return `waiting · resumes ${clock(date)}`;
|
|
4662
|
-
const day = date.toDateString();
|
|
4663
|
-
if (days <= WEEK_DAYS) return `waiting · resumes ${day.slice(0, 3)} ${clock(date)}`;
|
|
4664
|
-
return `waiting · resumes ${day.slice(4, 7)} ${String(date.getDate())} ${clock(date)}`;
|
|
4665
|
-
}
|
|
4666
4667
|
// src/health-actions.ts
|
|
4667
4668
|
function parseSharedHealth(raw) {
|
|
4668
4669
|
if (raw === void 0) return [];
|
|
@@ -4712,76 +4713,86 @@ const PI_COPY = {
|
|
|
4712
4713
|
receiptExplanation: (reason) => defer[reason],
|
|
4713
4714
|
parkExplanation: (reason) => park[reason]
|
|
4714
4715
|
};
|
|
4715
|
-
// src/
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4716
|
+
// ../core/src/flow/status.ts
|
|
4717
|
+
/**
|
|
4718
|
+
* Status-view helpers (ADP-033): flatten the status snapshot into one page
|
|
4719
|
+
* showing every configured model with its state, grouped by tier in priority
|
|
4720
|
+
* order. Pure and deterministic; local wall-clock rendering is derived from
|
|
4721
|
+
* the snapshot's own timestamps.
|
|
4722
|
+
*/
|
|
4723
|
+
/** Configured resources in tier order (quick → standard → advanced). */
|
|
4724
|
+
function healthEntries(snapshot) {
|
|
4725
|
+
const entries = [];
|
|
4726
|
+
for (const tier of TIERS) for (const status of snapshot.tiers[tier]) entries.push({
|
|
4727
|
+
tier,
|
|
4728
|
+
status
|
|
4729
|
+
});
|
|
4730
|
+
return entries;
|
|
4724
4731
|
}
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4732
|
+
/** Local date and time: quota windows may reset days or weeks from now. */
|
|
4733
|
+
function clockTime(until) {
|
|
4734
|
+
const at = new Date(until);
|
|
4735
|
+
const hours = String(at.getHours()).padStart(2, "0");
|
|
4736
|
+
const minutes = String(at.getMinutes()).padStart(2, "0");
|
|
4737
|
+
const month = String(at.getMonth() + 1).padStart(2, "0");
|
|
4738
|
+
const day = String(at.getDate()).padStart(2, "0");
|
|
4739
|
+
return `${String(at.getFullYear())}-${month}-${day} ${hours}:${minutes}`;
|
|
4740
|
+
}
|
|
4741
|
+
function pinnedTo(ref, pin) {
|
|
4742
|
+
if (pin === null) return false;
|
|
4743
|
+
return pin.providerID === ref.providerID && pin.id === ref.id && pin.variant === ref.variant;
|
|
4744
|
+
}
|
|
4745
|
+
/** Everything after the health state: capacity usage, window, breaker, pin. */
|
|
4746
|
+
function stateSuffix(status, pin) {
|
|
4747
|
+
let suffix = "";
|
|
4748
|
+
if (status.windowed) suffix += status.windowUntil === null ? ", windowed (deadline unknown)" : `, windowed until ${clockTime(status.windowUntil)}`;
|
|
4749
|
+
if (status.usedPercent !== null) {
|
|
4750
|
+
suffix += `, used ${String(status.usedPercent)}%`;
|
|
4751
|
+
if (status.resetAt !== null) suffix += `, resets ${clockTime(status.resetAt)}`;
|
|
4733
4752
|
}
|
|
4734
|
-
|
|
4753
|
+
if (status.breaker === "open") suffix += ", breaker open";
|
|
4754
|
+
else if (status.breaker === "half_open") suffix += ", probing";
|
|
4755
|
+
if (pinnedTo(status.ref, pin)) suffix += " (pinned)";
|
|
4756
|
+
return suffix;
|
|
4735
4757
|
}
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
const tested = testedUpTo === void 0 ? void 0 : parts(testedUpTo);
|
|
4740
|
-
if (actual === void 0 || floor === void 0) return [{
|
|
4741
|
-
state: "warning",
|
|
4742
|
-
message: `Pi ${version}: version support could not be checked`,
|
|
4743
|
-
action: "Pick a normal model and report this version."
|
|
4744
|
-
}];
|
|
4745
|
-
if (actual[0] !== 1 || compare(actual, floor) < 0) return [{
|
|
4746
|
-
state: "error",
|
|
4747
|
-
message: `Pi ${version} is unsupported (requires ≥${minimum} <2)`,
|
|
4748
|
-
action: "Install a supported Pi version or pick a normal model."
|
|
4749
|
-
}];
|
|
4750
|
-
const checks = [{
|
|
4751
|
-
state: "ok",
|
|
4752
|
-
message: `Pi ${version} (supported: ≥${minimum} <2)`
|
|
4753
|
-
}];
|
|
4754
|
-
if (tested === void 0) checks.push({
|
|
4755
|
-
state: "warning",
|
|
4756
|
-
message: "Packed-host tested-up-to evidence is not recorded",
|
|
4757
|
-
action: "Run the complete Pi host matrix before release."
|
|
4758
|
-
});
|
|
4759
|
-
else if (compare(actual, tested) > 0) checks.push({
|
|
4760
|
-
state: "warning",
|
|
4761
|
-
message: `Pi ${version} is newer than last tested ${String(testedUpTo)}`,
|
|
4762
|
-
action: "If routing misbehaves, pick a normal model and report it."
|
|
4763
|
-
});
|
|
4764
|
-
else checks.push({
|
|
4765
|
-
state: "ok",
|
|
4766
|
-
message: `Packed hosts tested up to ${String(testedUpTo)}`
|
|
4767
|
-
});
|
|
4768
|
-
return checks;
|
|
4758
|
+
/** The STATE column of the status table: window, breaker, pin markers. */
|
|
4759
|
+
function stateCell(status, pin) {
|
|
4760
|
+
return stateSuffix(status, pin).replace(/^[, ]+/, "") || "-";
|
|
4769
4761
|
}
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4762
|
+
/**
|
|
4763
|
+
* The whole inventory as a column-aligned table (header row first): models
|
|
4764
|
+
* grouped in tier order (quick → standard → advanced), one row per model,
|
|
4765
|
+
* columns padded to the widest cell so the grid reads at a glance. There is
|
|
4766
|
+
* no per-resource selection step: the table IS the view.
|
|
4767
|
+
*/
|
|
4768
|
+
function statusTable(snapshot) {
|
|
4769
|
+
const header = [
|
|
4770
|
+
"MODEL",
|
|
4771
|
+
"TIER",
|
|
4772
|
+
"HEALTH",
|
|
4773
|
+
"STATE"
|
|
4774
|
+
];
|
|
4775
|
+
const rows = healthEntries(snapshot).map(({ tier, status }) => [
|
|
4776
|
+
resourceLabel(status.ref),
|
|
4777
|
+
tier,
|
|
4778
|
+
status.health,
|
|
4779
|
+
stateCell(status, snapshot.pin)
|
|
4780
|
+
]);
|
|
4781
|
+
const widths = header.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column].length)));
|
|
4782
|
+
const render = (cells) => cells.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd();
|
|
4783
|
+
return [render(header), ...rows.map(render)];
|
|
4784
4784
|
}
|
|
4785
|
+
/** Narrow hosts keep health and capacity visible instead of clipping wide model columns. */
|
|
4786
|
+
function compactStatus(snapshot) {
|
|
4787
|
+
return healthEntries(snapshot).flatMap(({ tier, status }) => [
|
|
4788
|
+
resourceLabel(status.ref),
|
|
4789
|
+
`${tier} · ${status.health}`,
|
|
4790
|
+
...stateCell(status, snapshot.pin).split(", ").map((part) => ` ${part}`),
|
|
4791
|
+
""
|
|
4792
|
+
]);
|
|
4793
|
+
}
|
|
4794
|
+
/** Empty-state copy for the status view when nothing is configured. */
|
|
4795
|
+
const STATUS_EMPTY_MESSAGE = "No models configured yet — open Models to add some.";
|
|
4785
4796
|
// src/capabilities.ts
|
|
4786
4797
|
function object(value) {
|
|
4787
4798
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -4829,6 +4840,22 @@ function parseToolCapabilities(raw) {
|
|
|
4829
4840
|
};
|
|
4830
4841
|
});
|
|
4831
4842
|
}
|
|
4843
|
+
function updateToolCapability(file, ref, tools) {
|
|
4844
|
+
return file.update((current) => {
|
|
4845
|
+
const entries = current === void 0 ? [] : parseToolCapabilities(current);
|
|
4846
|
+
const entry = {
|
|
4847
|
+
providerID: ref.providerID,
|
|
4848
|
+
id: ref.id,
|
|
4849
|
+
tools
|
|
4850
|
+
};
|
|
4851
|
+
const models = [...entries.filter((model) => model.providerID !== ref.providerID || model.id !== ref.id), entry];
|
|
4852
|
+
parseToolCapabilities({ models });
|
|
4853
|
+
return {
|
|
4854
|
+
next: { models },
|
|
4855
|
+
result: void 0
|
|
4856
|
+
};
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4832
4859
|
// src/host/actions.ts
|
|
4833
4860
|
const DEFAULT_MODEL_HELP = "In Pi settings.json, set:\n{\n \"defaultProvider\": \"adaptive\",\n \"defaultModel\": \"auto\"\n}\nPi exposes no extension API to save this default. Your current model is unchanged.";
|
|
4834
4861
|
async function preferencesAction(view, file) {
|
|
@@ -4862,103 +4889,266 @@ async function preferencesAction(view, file) {
|
|
|
4862
4889
|
} else return;
|
|
4863
4890
|
view.toast("Preferences saved", "success");
|
|
4864
4891
|
}
|
|
4865
|
-
|
|
4892
|
+
function declaredLabel(declared, ref) {
|
|
4893
|
+
const entry = declared.find((model) => model.providerID === ref.providerID && model.id === ref.id);
|
|
4894
|
+
if (entry === void 0) return "not declared · skipped on tool turns";
|
|
4895
|
+
return entry.tools ? "calls tools" : "no tool calls";
|
|
4896
|
+
}
|
|
4897
|
+
/**
|
|
4898
|
+
* Pi's catalog does not say which models call tools, and Adaptive never
|
|
4899
|
+
* guesses from a name: an undeclared model is skipped on every tool turn.
|
|
4900
|
+
*/
|
|
4901
|
+
async function toolSupportAction(view, file, models, declared) {
|
|
4902
|
+
const unique = [...new Map(models.map((model) => [`${model.providerID}/${model.id}`, {
|
|
4903
|
+
providerID: model.providerID,
|
|
4904
|
+
id: model.id
|
|
4905
|
+
}]))];
|
|
4906
|
+
if (unique.length === 0) {
|
|
4907
|
+
await view.alert({
|
|
4908
|
+
title: "Tool support",
|
|
4909
|
+
message: STATUS_EMPTY_MESSAGE
|
|
4910
|
+
});
|
|
4911
|
+
return;
|
|
4912
|
+
}
|
|
4866
4913
|
const ref = await view.select({
|
|
4867
|
-
title: "
|
|
4868
|
-
options:
|
|
4869
|
-
title:
|
|
4914
|
+
title: "Tool support · select a model",
|
|
4915
|
+
options: unique.map(([label, model]) => ({
|
|
4916
|
+
title: label,
|
|
4870
4917
|
value: model,
|
|
4871
|
-
description:
|
|
4918
|
+
description: declaredLabel(declared, model)
|
|
4872
4919
|
}))
|
|
4873
4920
|
});
|
|
4874
4921
|
if (ref === void 0) return;
|
|
4922
|
+
const tools = await view.select({
|
|
4923
|
+
title: `${ref.providerID}/${ref.id}`,
|
|
4924
|
+
options: [{
|
|
4925
|
+
title: "Calls tools",
|
|
4926
|
+
value: true,
|
|
4927
|
+
description: "eligible for tool turns"
|
|
4928
|
+
}, {
|
|
4929
|
+
title: "No tool calls",
|
|
4930
|
+
value: false,
|
|
4931
|
+
description: "never routed to on tool turns"
|
|
4932
|
+
}]
|
|
4933
|
+
});
|
|
4934
|
+
if (tools === void 0) return;
|
|
4875
4935
|
if (!await view.confirm({
|
|
4876
|
-
title: "
|
|
4877
|
-
message:
|
|
4936
|
+
title: "Confirm tool capability",
|
|
4937
|
+
message: `${ref.providerID}/${ref.id}: record tool support as ${tools ? "supported" : "unsupported"}? Only confirm this from provider documentation or a verified request.`
|
|
4878
4938
|
})) return;
|
|
4879
|
-
await
|
|
4880
|
-
view.toast("
|
|
4939
|
+
await updateToolCapability(file, ref, tools);
|
|
4940
|
+
view.toast("Tool capability saved", "success");
|
|
4881
4941
|
}
|
|
4882
|
-
// ../core/src/flow/
|
|
4942
|
+
// ../core/src/flow/doctor.ts
|
|
4883
4943
|
/**
|
|
4884
|
-
*
|
|
4885
|
-
*
|
|
4886
|
-
*
|
|
4887
|
-
* the snapshot's own timestamps.
|
|
4944
|
+
* Doctor report (ADP-062): host checks plus the checks every host can derive
|
|
4945
|
+
* from the status snapshot, rendered problems-first so the first screen
|
|
4946
|
+
* answers "is anything wrong, and what do I do".
|
|
4888
4947
|
*/
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
const entries = [];
|
|
4892
|
-
for (const tier of TIERS) for (const status of snapshot.tiers[tier]) entries.push({
|
|
4893
|
-
tier,
|
|
4894
|
-
status
|
|
4895
|
-
});
|
|
4896
|
-
return entries;
|
|
4948
|
+
function plural(count, noun) {
|
|
4949
|
+
return `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
4897
4950
|
}
|
|
4898
|
-
/**
|
|
4899
|
-
function
|
|
4900
|
-
const
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4951
|
+
/** Empty tiers, limited models and parked work, from one status snapshot. */
|
|
4952
|
+
function statusChecks(snapshot) {
|
|
4953
|
+
const empty = TIERS.filter((tier) => snapshot.tiers[tier].length === 0).map((tier) => ({
|
|
4954
|
+
state: "warning",
|
|
4955
|
+
message: `${tier}: no configured models`,
|
|
4956
|
+
action: "Open /adaptive tiers to add a model."
|
|
4957
|
+
}));
|
|
4958
|
+
const limited = new Set(healthEntries(snapshot).filter(({ status }) => status.health !== "healthy").map(({ status }) => resourceLabel(status.ref))).size;
|
|
4959
|
+
return [
|
|
4960
|
+
...empty,
|
|
4961
|
+
limited === 0 ? {
|
|
4962
|
+
state: "ok",
|
|
4963
|
+
message: "Health: no models limited"
|
|
4964
|
+
} : {
|
|
4965
|
+
state: "ok",
|
|
4966
|
+
message: `Health: ${plural(limited, "model")} limited`,
|
|
4967
|
+
action: "Inspect /adaptive health; reset only after checking the provider."
|
|
4968
|
+
},
|
|
4969
|
+
snapshot.parked === 0 ? {
|
|
4970
|
+
state: "ok",
|
|
4971
|
+
message: "No parked work"
|
|
4972
|
+
} : {
|
|
4973
|
+
state: "ok",
|
|
4974
|
+
message: plural(snapshot.parked, "parked task"),
|
|
4975
|
+
action: "Use /adaptive resume or /adaptive cancel in the waiting session."
|
|
4976
|
+
}
|
|
4977
|
+
];
|
|
4906
4978
|
}
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4979
|
+
const LABELS = {
|
|
4980
|
+
ok: "OK",
|
|
4981
|
+
warning: "CHECK",
|
|
4982
|
+
error: "FIX"
|
|
4983
|
+
};
|
|
4984
|
+
const SEVERITY = [
|
|
4985
|
+
"error",
|
|
4986
|
+
"warning",
|
|
4987
|
+
"ok"
|
|
4988
|
+
];
|
|
4989
|
+
/** Problems first: the first screen answers "is anything wrong, and what do I do". */
|
|
4990
|
+
function doctorLines(checks) {
|
|
4991
|
+
const count = (state) => checks.filter((check) => check.state === state).length;
|
|
4992
|
+
return [count("error") + count("warning") === 0 ? `All ${String(checks.length)} checks OK` : `${String(count("error"))} to fix · ${String(count("warning"))} to check · ${String(count("ok"))} OK`, ...SEVERITY.flatMap((state) => checks.filter((check) => check.state === state)).flatMap((check) => [`${LABELS[check.state]} ${check.message}`, ...check.action === void 0 ? [] : [` ${check.action}`]])];
|
|
4910
4993
|
}
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4994
|
+
// ../core/src/flow/waiting.ts
|
|
4995
|
+
const DAY_MS = 864e5;
|
|
4996
|
+
const WEEK_DAYS = 6;
|
|
4997
|
+
function startOfDay(at) {
|
|
4998
|
+
const date = new Date(at);
|
|
4999
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
5000
|
+
}
|
|
5001
|
+
function clock(date) {
|
|
5002
|
+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
5003
|
+
}
|
|
5004
|
+
/**
|
|
5005
|
+
* Short human text for a parked session (ADP-024): "waiting · resumes 18:42". Weekly limits reset days away, so
|
|
5006
|
+
* a later day gains its weekday, and beyond a week its date. Local fields, not the locale formatter, keep the
|
|
5007
|
+
* text identical across hosts.
|
|
5008
|
+
*/
|
|
5009
|
+
function waitingLabel(resumeAt, now) {
|
|
5010
|
+
if (resumeAt === null) return "waiting";
|
|
5011
|
+
if (resumeAt <= now) return "waiting · resuming";
|
|
5012
|
+
const date = new Date(resumeAt);
|
|
5013
|
+
const days = Math.round((startOfDay(resumeAt) - startOfDay(now)) / DAY_MS);
|
|
5014
|
+
if (days === 0) return `waiting · resumes ${clock(date)}`;
|
|
5015
|
+
const day = date.toDateString();
|
|
5016
|
+
if (days <= WEEK_DAYS) return `waiting · resumes ${day.slice(0, 3)} ${clock(date)}`;
|
|
5017
|
+
return `waiting · resumes ${day.slice(4, 7)} ${String(date.getDate())} ${clock(date)}`;
|
|
5018
|
+
}
|
|
5019
|
+
// ../core/src/flow/actions.ts
|
|
5020
|
+
const PARKED_TITLE = "Parked work";
|
|
5021
|
+
const NOTHING_PARKED = "Nothing is parked in this session.";
|
|
5022
|
+
const OPEN_SESSION = "Open the waiting session and run /adaptive there to resume or cancel it.";
|
|
5023
|
+
/** Pick a configured model, confirm, reset. Lists each model once, with its state. */
|
|
5024
|
+
async function runHealthReset(env, reset) {
|
|
5025
|
+
const snapshot = await env.api.status();
|
|
5026
|
+
const models = /* @__PURE__ */ new Map();
|
|
5027
|
+
for (const { status } of healthEntries(snapshot)) models.set(resourceLabel(status.ref), status);
|
|
5028
|
+
if (models.size === 0) {
|
|
5029
|
+
await env.view.alert({
|
|
5030
|
+
title: "Health",
|
|
5031
|
+
message: STATUS_EMPTY_MESSAGE
|
|
5032
|
+
});
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
const model = await env.view.select({
|
|
5036
|
+
title: "Health · select a model to reset",
|
|
5037
|
+
options: [...models].map(([label, status]) => {
|
|
5038
|
+
const state = stateCell(status, snapshot.pin);
|
|
5039
|
+
return {
|
|
5040
|
+
title: label,
|
|
5041
|
+
value: status.ref,
|
|
5042
|
+
description: state === "-" ? status.health : `${status.health} · ${state}`
|
|
5043
|
+
};
|
|
5044
|
+
})
|
|
5045
|
+
});
|
|
5046
|
+
if (model === void 0) return;
|
|
5047
|
+
const label = resourceLabel(model);
|
|
5048
|
+
if (await env.view.confirm({
|
|
5049
|
+
title: "Reset model health?",
|
|
5050
|
+
message: `Reset ${label} and its inherited provider limits? Other models on this provider may be affected. This does not restore actual provider quota.`
|
|
5051
|
+
}) !== true) return;
|
|
5052
|
+
await reset(model);
|
|
5053
|
+
env.view.toast("Health reset", "success");
|
|
5054
|
+
}
|
|
5055
|
+
/** Resume this session's parked work. True when the run continues and the menu should close. */
|
|
5056
|
+
async function resumeHere(env, resume) {
|
|
5057
|
+
if (env.sessionID === void 0) {
|
|
5058
|
+
await env.view.alert({
|
|
5059
|
+
title: PARKED_TITLE,
|
|
5060
|
+
message: OPEN_SESSION
|
|
5061
|
+
});
|
|
5062
|
+
return false;
|
|
5063
|
+
}
|
|
5064
|
+
switch (await resume(env.sessionID)) {
|
|
5065
|
+
case "resumed":
|
|
5066
|
+
env.view.toast("Resuming parked work", "success");
|
|
5067
|
+
return true;
|
|
5068
|
+
case "waiting":
|
|
5069
|
+
await env.view.alert({
|
|
5070
|
+
title: PARKED_TITLE,
|
|
5071
|
+
message: "No model in this tier is available yet. Adaptive keeps waiting and retries."
|
|
5072
|
+
});
|
|
5073
|
+
return false;
|
|
5074
|
+
case "not-parked":
|
|
5075
|
+
await env.view.alert({
|
|
5076
|
+
title: PARKED_TITLE,
|
|
5077
|
+
message: NOTHING_PARKED
|
|
5078
|
+
});
|
|
5079
|
+
return false;
|
|
4918
5080
|
}
|
|
4919
|
-
if (status.breaker === "open") suffix += ", breaker open";
|
|
4920
|
-
else if (status.breaker === "half_open") suffix += ", probing";
|
|
4921
|
-
if (pinnedTo(status.ref, pin)) suffix += " (pinned)";
|
|
4922
|
-
return suffix;
|
|
4923
5081
|
}
|
|
4924
|
-
/**
|
|
4925
|
-
function
|
|
4926
|
-
|
|
5082
|
+
/** Drop this session's parked work. */
|
|
5083
|
+
async function cancelHere(env, cancel) {
|
|
5084
|
+
if (env.sessionID === void 0) {
|
|
5085
|
+
await env.view.alert({
|
|
5086
|
+
title: PARKED_TITLE,
|
|
5087
|
+
message: OPEN_SESSION
|
|
5088
|
+
});
|
|
5089
|
+
return;
|
|
5090
|
+
}
|
|
5091
|
+
if (await cancel(env.sessionID) === "cancelled") env.view.toast("Parked work cancelled", "success");
|
|
5092
|
+
else await env.view.alert({
|
|
5093
|
+
title: PARKED_TITLE,
|
|
5094
|
+
message: NOTHING_PARKED
|
|
5095
|
+
});
|
|
4927
5096
|
}
|
|
4928
5097
|
/**
|
|
4929
|
-
*
|
|
4930
|
-
*
|
|
4931
|
-
*
|
|
4932
|
-
* no per-resource selection step: the table IS the view.
|
|
5098
|
+
* Waiting sessions, then what to do with this session's wait. Other sessions
|
|
5099
|
+
* are listed but act only from inside: a host can only continue the session
|
|
5100
|
+
* it runs in. True when a resumed run continues and the menu should close.
|
|
4933
5101
|
*/
|
|
4934
|
-
function
|
|
4935
|
-
const
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5102
|
+
async function runParkedWork(env, actions) {
|
|
5103
|
+
const { waiting } = await env.api.status();
|
|
5104
|
+
if (waiting.length === 0) {
|
|
5105
|
+
await env.view.alert({
|
|
5106
|
+
title: PARKED_TITLE,
|
|
5107
|
+
message: "No parked work."
|
|
5108
|
+
});
|
|
5109
|
+
return false;
|
|
5110
|
+
}
|
|
5111
|
+
const now = (env.clock ?? Date.now)();
|
|
5112
|
+
const picked = await env.view.select({
|
|
5113
|
+
title: PARKED_TITLE,
|
|
5114
|
+
options: waiting.map((entry) => ({
|
|
5115
|
+
title: entry.sessionID === env.sessionID ? "This session" : entry.sessionID,
|
|
5116
|
+
value: entry.sessionID,
|
|
5117
|
+
description: `${entry.tier} · ${waitingLabel(entry.resumeAt, now)}`
|
|
5118
|
+
}))
|
|
5119
|
+
});
|
|
5120
|
+
if (picked === void 0) return false;
|
|
5121
|
+
if (picked !== env.sessionID) {
|
|
5122
|
+
await env.view.alert({
|
|
5123
|
+
title: PARKED_TITLE,
|
|
5124
|
+
message: OPEN_SESSION
|
|
5125
|
+
});
|
|
5126
|
+
return false;
|
|
5127
|
+
}
|
|
5128
|
+
const { resume, cancel } = actions;
|
|
5129
|
+
const run = await env.view.select({
|
|
5130
|
+
title: "This session's parked work",
|
|
5131
|
+
options: [...resume === void 0 ? [] : [{
|
|
5132
|
+
title: "Resume now",
|
|
5133
|
+
value: () => resumeHere(env, resume),
|
|
5134
|
+
description: "try the tier again"
|
|
5135
|
+
}], ...cancel === void 0 ? [] : [{
|
|
5136
|
+
title: "Cancel",
|
|
5137
|
+
value: async () => {
|
|
5138
|
+
await cancelHere(env, cancel);
|
|
5139
|
+
return false;
|
|
5140
|
+
},
|
|
5141
|
+
description: "drop the wait; your next prompt starts fresh"
|
|
5142
|
+
}]]
|
|
5143
|
+
});
|
|
5144
|
+
return run === void 0 ? false : run();
|
|
4950
5145
|
}
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
...stateCell(status, snapshot.pin).split(", ").map((part) => ` ${part}`),
|
|
4957
|
-
""
|
|
4958
|
-
]);
|
|
5146
|
+
async function runDoctor(env, doctor) {
|
|
5147
|
+
await env.view.table({
|
|
5148
|
+
title: "Adaptive doctor",
|
|
5149
|
+
lines: doctorLines(await doctor())
|
|
5150
|
+
});
|
|
4959
5151
|
}
|
|
4960
|
-
/** Empty-state copy for the status view when nothing is configured. */
|
|
4961
|
-
const STATUS_EMPTY_MESSAGE = "No models configured yet — open Models to add some.";
|
|
4962
5152
|
// ../core/src/flow/menu.ts
|
|
4963
5153
|
/**
|
|
4964
5154
|
* Main-menu and mode-menu view models for `/adaptive` (ADP-033): pure data
|
|
@@ -5293,6 +5483,46 @@ async function applyTierRequest(env, sessionID, tier, feedback) {
|
|
|
5293
5483
|
}
|
|
5294
5484
|
return result;
|
|
5295
5485
|
}
|
|
5486
|
+
function actionOptions(env) {
|
|
5487
|
+
const actions = env.actions ?? {};
|
|
5488
|
+
const { resetHealth, resume, cancel, tools, doctor, preferences, defaults } = actions;
|
|
5489
|
+
const done = (run) => async () => {
|
|
5490
|
+
await run();
|
|
5491
|
+
return false;
|
|
5492
|
+
};
|
|
5493
|
+
return [
|
|
5494
|
+
resetHealth !== void 0 && {
|
|
5495
|
+
title: "Health",
|
|
5496
|
+
value: done(() => runHealthReset(env, resetHealth)),
|
|
5497
|
+
description: "reset a model marked limited"
|
|
5498
|
+
},
|
|
5499
|
+
(resume !== void 0 || cancel !== void 0) && {
|
|
5500
|
+
title: "Parked work",
|
|
5501
|
+
value: () => runParkedWork(env, actions),
|
|
5502
|
+
description: "resume or cancel waiting work"
|
|
5503
|
+
},
|
|
5504
|
+
tools !== void 0 && {
|
|
5505
|
+
title: "Tool support",
|
|
5506
|
+
value: done(tools),
|
|
5507
|
+
description: "declare which models can call tools"
|
|
5508
|
+
},
|
|
5509
|
+
doctor !== void 0 && {
|
|
5510
|
+
title: "Doctor",
|
|
5511
|
+
value: done(() => runDoctor(env, doctor)),
|
|
5512
|
+
description: "check setup and compatibility"
|
|
5513
|
+
},
|
|
5514
|
+
preferences !== void 0 && {
|
|
5515
|
+
title: "Preferences",
|
|
5516
|
+
value: done(preferences),
|
|
5517
|
+
description: "transcript notes and pinned-tier escalation"
|
|
5518
|
+
},
|
|
5519
|
+
defaults !== void 0 && {
|
|
5520
|
+
title: "Use Adaptive for new sessions",
|
|
5521
|
+
value: done(defaults),
|
|
5522
|
+
description: "Pi settings to copy"
|
|
5523
|
+
}
|
|
5524
|
+
].filter((row) => row !== false);
|
|
5525
|
+
}
|
|
5296
5526
|
async function runAdaptive(env, initial) {
|
|
5297
5527
|
const loaded = await tryApi(env, () => env.api.settings());
|
|
5298
5528
|
if (loaded === void 0) return;
|
|
@@ -5326,40 +5556,12 @@ async function runAdaptive(env, initial) {
|
|
|
5326
5556
|
const tierPinned = sessionID === void 0 ? void 0 : await env.api.tierRequest(sessionID).catch(() => void 0);
|
|
5327
5557
|
const choice = next ?? await env.view.select({
|
|
5328
5558
|
title: `Adaptive (${override ?? settings.mode})`,
|
|
5329
|
-
options: [...mainMenuOptions(settings, status, tierPinned, override ?? settings.mode), ...env
|
|
5330
|
-
{
|
|
5331
|
-
title: "Health",
|
|
5332
|
-
value: "health",
|
|
5333
|
-
description: "inspect or reset resource health"
|
|
5334
|
-
},
|
|
5335
|
-
{
|
|
5336
|
-
title: "Parked work",
|
|
5337
|
-
value: "parked",
|
|
5338
|
-
description: "resume or cancel waiting work"
|
|
5339
|
-
},
|
|
5340
|
-
{
|
|
5341
|
-
title: "Doctor",
|
|
5342
|
-
value: "doctor",
|
|
5343
|
-
description: "check setup and compatibility"
|
|
5344
|
-
},
|
|
5345
|
-
{
|
|
5346
|
-
title: "Preferences",
|
|
5347
|
-
value: "preferences",
|
|
5348
|
-
description: "transcript notes and pinned-tier escalation"
|
|
5349
|
-
},
|
|
5350
|
-
{
|
|
5351
|
-
title: "Use Adaptive for new sessions",
|
|
5352
|
-
value: "defaults",
|
|
5353
|
-
description: "Pi settings to copy"
|
|
5354
|
-
}
|
|
5355
|
-
]]
|
|
5559
|
+
options: [...mainMenuOptions(settings, status, tierPinned, override ?? settings.mode), ...actionOptions(env)]
|
|
5356
5560
|
});
|
|
5357
5561
|
next = void 0;
|
|
5358
5562
|
if (choice === void 0) return;
|
|
5359
|
-
if (
|
|
5360
|
-
await tryApi(env,
|
|
5361
|
-
await env.actions?.[choice]();
|
|
5362
|
-
});
|
|
5563
|
+
if (typeof choice === "function") {
|
|
5564
|
+
if (await tryApi(env, choice) === true) return;
|
|
5363
5565
|
continue;
|
|
5364
5566
|
}
|
|
5365
5567
|
if (choice === "status") {
|
|
@@ -5675,11 +5877,17 @@ async function runReceiptsView(env) {
|
|
|
5675
5877
|
});
|
|
5676
5878
|
}
|
|
5677
5879
|
}
|
|
5678
|
-
// src/
|
|
5679
|
-
|
|
5880
|
+
// ../core/src/flow/command.ts
|
|
5881
|
+
/**
|
|
5882
|
+
* `/adaptive <subcommand>`, shared by every host (ADP-062): one parser, one
|
|
5883
|
+
* copy, so a subcommand typed in Pi behaves the same in OpenCode. Action-backed
|
|
5884
|
+
* subcommands exist only where the host wires the action.
|
|
5885
|
+
*/
|
|
5886
|
+
const ADAPTIVE_SUBCOMMANDS = [
|
|
5680
5887
|
"status",
|
|
5681
5888
|
"why",
|
|
5682
5889
|
"tiers",
|
|
5890
|
+
"tools",
|
|
5683
5891
|
"health",
|
|
5684
5892
|
"resume",
|
|
5685
5893
|
"cancel",
|
|
@@ -5687,49 +5895,83 @@ const SUBCOMMANDS = [
|
|
|
5687
5895
|
"observe off",
|
|
5688
5896
|
"doctor"
|
|
5689
5897
|
];
|
|
5898
|
+
const REQUIRES = {
|
|
5899
|
+
tools: "tools",
|
|
5900
|
+
health: "resetHealth",
|
|
5901
|
+
resume: "resume",
|
|
5902
|
+
cancel: "cancel",
|
|
5903
|
+
doctor: "doctor"
|
|
5904
|
+
};
|
|
5905
|
+
/** The subcommands a host with these actions can run, in menu order. */
|
|
5906
|
+
function adaptiveSubcommands(actions) {
|
|
5907
|
+
return ADAPTIVE_SUBCOMMANDS.filter((command) => {
|
|
5908
|
+
const needs = REQUIRES[command];
|
|
5909
|
+
return needs === void 0 || actions?.[needs] !== void 0;
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
/** Runs `/adaptive <input>`; an empty input opens the menu. API failures propagate to the host. */
|
|
5913
|
+
async function runAdaptiveCommand(env, input) {
|
|
5914
|
+
const command = input.trim().replace(/\s+/g, " ");
|
|
5915
|
+
const { resetHealth, resume, cancel, tools, doctor } = env.actions ?? {};
|
|
5916
|
+
switch (command) {
|
|
5917
|
+
case "":
|
|
5918
|
+
await runAdaptive(env);
|
|
5919
|
+
return;
|
|
5920
|
+
case "status":
|
|
5921
|
+
await runAdaptive(env, "status");
|
|
5922
|
+
return;
|
|
5923
|
+
case "tiers":
|
|
5924
|
+
await runAdaptive(env, "models");
|
|
5925
|
+
return;
|
|
5926
|
+
case "why": {
|
|
5927
|
+
const latest = (await env.api.receipts()).find((receipt) => receipt.sessionID === env.sessionID);
|
|
5928
|
+
await env.view.alert({
|
|
5929
|
+
title: "Latest decision",
|
|
5930
|
+
message: latest === void 0 ? "No decisions recorded in this session yet." : receiptDetail(latest, env.copy, (env.clock ?? Date.now)())
|
|
5931
|
+
});
|
|
5932
|
+
return;
|
|
5933
|
+
}
|
|
5934
|
+
case "observe on":
|
|
5935
|
+
case "observe off": {
|
|
5936
|
+
const result = await env.api.setSettings({ mode: command === "observe on" ? "observe" : "off" });
|
|
5937
|
+
env.view.toast(result.ok ? "Observation setting saved. Your selected model is unchanged." : result.errors.join("; "), result.ok ? "success" : "error");
|
|
5938
|
+
return;
|
|
5939
|
+
}
|
|
5940
|
+
case "tools":
|
|
5941
|
+
if (tools === void 0) break;
|
|
5942
|
+
await tools();
|
|
5943
|
+
return;
|
|
5944
|
+
case "health":
|
|
5945
|
+
if (resetHealth === void 0) break;
|
|
5946
|
+
await runHealthReset(env, resetHealth);
|
|
5947
|
+
return;
|
|
5948
|
+
case "resume":
|
|
5949
|
+
if (resume === void 0) break;
|
|
5950
|
+
await resumeHere(env, resume);
|
|
5951
|
+
return;
|
|
5952
|
+
case "cancel":
|
|
5953
|
+
if (cancel === void 0) break;
|
|
5954
|
+
await cancelHere(env, cancel);
|
|
5955
|
+
return;
|
|
5956
|
+
case "doctor":
|
|
5957
|
+
if (doctor === void 0) break;
|
|
5958
|
+
await runDoctor(env, doctor);
|
|
5959
|
+
return;
|
|
5960
|
+
}
|
|
5961
|
+
env.view.toast(`Unknown adaptive command. Use: ${adaptiveSubcommands(env.actions).join(", ")}.`, "error");
|
|
5962
|
+
}
|
|
5963
|
+
// src/host/commands.ts
|
|
5690
5964
|
async function runCommand(args, ctx, options) {
|
|
5691
5965
|
if (!ctx.hasUI) {
|
|
5692
5966
|
ctx.ui.notify("Open /adaptive in an interactive or RPC session.", "warning");
|
|
5693
5967
|
return;
|
|
5694
5968
|
}
|
|
5695
|
-
|
|
5696
|
-
const env = await options.env(ctx);
|
|
5697
|
-
if (command === "resume") {
|
|
5698
|
-
await options.resume(ctx);
|
|
5699
|
-
return;
|
|
5700
|
-
}
|
|
5701
|
-
if (command === "cancel") {
|
|
5702
|
-
await options.cancel(ctx);
|
|
5703
|
-
return;
|
|
5704
|
-
}
|
|
5705
|
-
if (command === "doctor" || command === "health") {
|
|
5706
|
-
await env.actions?.[command]();
|
|
5707
|
-
return;
|
|
5708
|
-
}
|
|
5709
|
-
if (command === "observe on" || command === "observe off") {
|
|
5710
|
-
const result = await env.api.setSettings({ mode: command === "observe on" ? "observe" : "off" });
|
|
5711
|
-
env.view.toast(result.ok ? "Observation setting saved. Your selected model is unchanged." : result.errors.join("; "), result.ok ? "success" : "error");
|
|
5712
|
-
return;
|
|
5713
|
-
}
|
|
5714
|
-
if (command === "why") {
|
|
5715
|
-
const latest = (await env.api.receipts()).find((receipt) => receipt.sessionID === env.sessionID);
|
|
5716
|
-
await env.view.alert({
|
|
5717
|
-
title: "Latest decision",
|
|
5718
|
-
message: latest === void 0 ? "No decisions recorded in this session yet." : receiptDetail(latest, PI_COPY, options.clock())
|
|
5719
|
-
});
|
|
5720
|
-
return;
|
|
5721
|
-
}
|
|
5722
|
-
if (command === "" || command === "status" || command === "tiers") {
|
|
5723
|
-
if (command === "") await runAdaptive(env, void 0);
|
|
5724
|
-
else await runAdaptive(env, command === "tiers" ? "models" : "status");
|
|
5725
|
-
return;
|
|
5726
|
-
}
|
|
5727
|
-
env.view.toast(`Unknown adaptive command. Use: ${SUBCOMMANDS.join(", ")}.`, "error");
|
|
5969
|
+
await runAdaptiveCommand(await options.env(ctx), args);
|
|
5728
5970
|
}
|
|
5729
5971
|
function registerCommands(pi, options) {
|
|
5730
5972
|
pi.registerCommand("adaptive", {
|
|
5731
5973
|
description: "Adaptive settings, decisions and diagnostics",
|
|
5732
|
-
getArgumentCompletions: (prefix) =>
|
|
5974
|
+
getArgumentCompletions: (prefix) => ADAPTIVE_SUBCOMMANDS.filter((command) => command.startsWith(prefix)).map((command) => ({
|
|
5733
5975
|
value: command,
|
|
5734
5976
|
label: command
|
|
5735
5977
|
})),
|
|
@@ -5824,6 +6066,61 @@ function controlApi(options) {
|
|
|
5824
6066
|
}
|
|
5825
6067
|
};
|
|
5826
6068
|
}
|
|
6069
|
+
// src/ui/doctor.ts
|
|
6070
|
+
function parts(version) {
|
|
6071
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) return void 0;
|
|
6072
|
+
const [major, minor, patch] = version.split(".");
|
|
6073
|
+
return [
|
|
6074
|
+
Number(major),
|
|
6075
|
+
Number(minor),
|
|
6076
|
+
Number(patch)
|
|
6077
|
+
];
|
|
6078
|
+
}
|
|
6079
|
+
function compare(a, b) {
|
|
6080
|
+
for (const index of [
|
|
6081
|
+
0,
|
|
6082
|
+
1,
|
|
6083
|
+
2
|
|
6084
|
+
]) {
|
|
6085
|
+
const difference = a[index] - b[index];
|
|
6086
|
+
if (difference !== 0) return difference;
|
|
6087
|
+
}
|
|
6088
|
+
return 0;
|
|
6089
|
+
}
|
|
6090
|
+
function versionChecks(version, minimum, testedUpTo) {
|
|
6091
|
+
const actual = parts(version);
|
|
6092
|
+
const floor = parts(minimum);
|
|
6093
|
+
const tested = testedUpTo === void 0 ? void 0 : parts(testedUpTo);
|
|
6094
|
+
if (actual === void 0 || floor === void 0) return [{
|
|
6095
|
+
state: "warning",
|
|
6096
|
+
message: `Pi ${version}: version support could not be checked`,
|
|
6097
|
+
action: "Pick a normal model and report this version."
|
|
6098
|
+
}];
|
|
6099
|
+
if (actual[0] !== 1 || compare(actual, floor) < 0) return [{
|
|
6100
|
+
state: "error",
|
|
6101
|
+
message: `Pi ${version} is unsupported (requires ≥${minimum} <2)`,
|
|
6102
|
+
action: "Install a supported Pi version or pick a normal model."
|
|
6103
|
+
}];
|
|
6104
|
+
const checks = [{
|
|
6105
|
+
state: "ok",
|
|
6106
|
+
message: `Pi ${version} (supported: ≥${minimum} <2)`
|
|
6107
|
+
}];
|
|
6108
|
+
if (tested === void 0) checks.push({
|
|
6109
|
+
state: "warning",
|
|
6110
|
+
message: "Packed-host tested-up-to evidence is not recorded",
|
|
6111
|
+
action: "Run the complete Pi host matrix before release."
|
|
6112
|
+
});
|
|
6113
|
+
else if (compare(actual, tested) > 0) checks.push({
|
|
6114
|
+
state: "warning",
|
|
6115
|
+
message: `Pi ${version} is newer than last tested ${String(testedUpTo)}`,
|
|
6116
|
+
action: "If routing misbehaves, pick a normal model and report it."
|
|
6117
|
+
});
|
|
6118
|
+
else checks.push({
|
|
6119
|
+
state: "ok",
|
|
6120
|
+
message: `Packed hosts tested up to ${String(testedUpTo)}`
|
|
6121
|
+
});
|
|
6122
|
+
return checks;
|
|
6123
|
+
}
|
|
5827
6124
|
// src/host/doctor.ts
|
|
5828
6125
|
/** Probe failures use fixed copy: upstream errors/configs may contain credentials. */
|
|
5829
6126
|
async function doctorChecks(options) {
|
|
@@ -5842,27 +6139,20 @@ async function doctorChecks(options) {
|
|
|
5842
6139
|
const capabilities = await options.capabilities();
|
|
5843
6140
|
const credentialed = [];
|
|
5844
6141
|
const undeclared = /* @__PURE__ */ new Set();
|
|
5845
|
-
for (const tier of TIERS) {
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
6142
|
+
for (const tier of TIERS) for (const ref of settings.tiers[tier]) {
|
|
6143
|
+
const model = options.registry.find(ref.providerID, ref.id);
|
|
6144
|
+
if (model === void 0) checks.push({
|
|
6145
|
+
state: "error",
|
|
6146
|
+
message: `${resourceLabel(ref)} (${tier}): model missing`,
|
|
6147
|
+
action: "Refresh Pi's model catalog or remove this tier entry."
|
|
5850
6148
|
});
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
else if (!options.registry.hasConfiguredAuth(model)) checks.push({
|
|
5859
|
-
state: "error",
|
|
5860
|
-
message: `${resourceLabel(ref)} (${tier}): no credentials configured`,
|
|
5861
|
-
action: `Run /login ${ref.providerID}, or remove it from the tier.`
|
|
5862
|
-
});
|
|
5863
|
-
else credentialed.push(resourceLabel(ref));
|
|
5864
|
-
if (!capabilities.some((entry) => entry.providerID === ref.providerID && entry.id === ref.id && entry.tools)) undeclared.add(resourceLabel(ref));
|
|
5865
|
-
}
|
|
6149
|
+
else if (!options.registry.hasConfiguredAuth(model)) checks.push({
|
|
6150
|
+
state: "error",
|
|
6151
|
+
message: `${resourceLabel(ref)} (${tier}): no credentials configured`,
|
|
6152
|
+
action: `Run /login ${ref.providerID}, or remove it from the tier.`
|
|
6153
|
+
});
|
|
6154
|
+
else credentialed.push(resourceLabel(ref));
|
|
6155
|
+
if (!capabilities.some((entry) => entry.providerID === ref.providerID && entry.id === ref.id && entry.tools)) undeclared.add(resourceLabel(ref));
|
|
5866
6156
|
}
|
|
5867
6157
|
if (credentialed.length > 0) checks.push({
|
|
5868
6158
|
state: "ok",
|
|
@@ -5871,7 +6161,7 @@ async function doctorChecks(options) {
|
|
|
5871
6161
|
if (undeclared.size > 0) checks.push({
|
|
5872
6162
|
state: "warning",
|
|
5873
6163
|
message: `Tool support not declared: ${[...undeclared].join(", ")}`,
|
|
5874
|
-
action: "
|
|
6164
|
+
action: "Open /adaptive tools to declare them; Adaptive never guesses from a name."
|
|
5875
6165
|
});
|
|
5876
6166
|
} catch {
|
|
5877
6167
|
checks.push({
|
|
@@ -5909,31 +6199,12 @@ async function doctorChecks(options) {
|
|
|
5909
6199
|
});
|
|
5910
6200
|
}
|
|
5911
6201
|
try {
|
|
5912
|
-
|
|
5913
|
-
checks.push({
|
|
5914
|
-
state: "ok",
|
|
5915
|
-
message: limited === 0 ? "Health: no resources limited" : `Health: ${String(limited)} ${limited === 1 ? "resource" : "resources"} limited`,
|
|
5916
|
-
...limited === 0 ? {} : { action: "Inspect /adaptive health; reset only after checking the provider." }
|
|
5917
|
-
});
|
|
6202
|
+
checks.push(...statusChecks(await options.status()));
|
|
5918
6203
|
} catch {
|
|
5919
6204
|
checks.push({
|
|
5920
6205
|
state: "error",
|
|
5921
|
-
message: "
|
|
5922
|
-
action: "Inspect adaptive/health.json; do not delete
|
|
5923
|
-
});
|
|
5924
|
-
}
|
|
5925
|
-
try {
|
|
5926
|
-
const parked = await options.parked();
|
|
5927
|
-
checks.push({
|
|
5928
|
-
state: "ok",
|
|
5929
|
-
message: parked === 0 ? "No parked work" : `${String(parked)} parked ${parked === 1 ? "task" : "tasks"}`,
|
|
5930
|
-
...parked === 0 ? {} : { action: "Use /adaptive resume or /adaptive cancel in the waiting session." }
|
|
5931
|
-
});
|
|
5932
|
-
} catch {
|
|
5933
|
-
checks.push({
|
|
5934
|
-
state: "error",
|
|
5935
|
-
message: "Parked work could not be read",
|
|
5936
|
-
action: "Inspect adaptive/parked.json before resuming; no task was dispatched."
|
|
6206
|
+
message: "Health or parked work could not be read",
|
|
6207
|
+
action: "Inspect adaptive/health.json and parked.json; do not delete them while another Pi process is running."
|
|
5937
6208
|
});
|
|
5938
6209
|
}
|
|
5939
6210
|
return checks;
|
|
@@ -5955,6 +6226,9 @@ function order(left, right) {
|
|
|
5955
6226
|
if (l !== r) return l < r ? -1 : 1;
|
|
5956
6227
|
return Number(left.reasoning) - Number(right.reasoning) || left.contextWindow - right.contextWindow || Number(left.thinkingLevels.includes("high")) - Number(right.thinkingLevels.includes("high"));
|
|
5957
6228
|
}
|
|
6229
|
+
/** A proposal is a starting point, not the catalog: a primary and a fallback per tier keep
|
|
6230
|
+
* the first-run decision small; the tier editor offers every configured model. */
|
|
6231
|
+
const PROPOSAL_TIER_CAP = 2;
|
|
5958
6232
|
/** Settings bootstrap only: never consulted during routing, and never persists. */
|
|
5959
6233
|
function proposeTiers(models) {
|
|
5960
6234
|
const unique = /* @__PURE__ */ new Map();
|
|
@@ -5962,7 +6236,7 @@ function proposeTiers(models) {
|
|
|
5962
6236
|
const ordered = [...unique.values()];
|
|
5963
6237
|
const first = Math.ceil(ordered.length / 3);
|
|
5964
6238
|
const second = Math.ceil(ordered.length * 2 / 3);
|
|
5965
|
-
const refs = (group, advanced) => group.slice(0,
|
|
6239
|
+
const refs = (group, advanced) => group.slice(0, PROPOSAL_TIER_CAP).map((model) => ({
|
|
5966
6240
|
providerID: model.ref.providerID,
|
|
5967
6241
|
id: model.ref.id,
|
|
5968
6242
|
...advanced && model.reasoning && model.thinkingLevels.includes("high") ? { variant: "high" } : {}
|
|
@@ -6368,38 +6642,27 @@ function adaptiveSurface(pi, sources, host, parking, feedback) {
|
|
|
6368
6642
|
view,
|
|
6369
6643
|
models: () => modelSummaries(availablePhysicalModels(ctx.modelRegistry)),
|
|
6370
6644
|
sessionID: ctx.sessionManager.getSessionId(),
|
|
6645
|
+
clock: sources.parking.clock,
|
|
6371
6646
|
actions: {
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
const now = sources.parking.clock();
|
|
6378
|
-
await view.alert({
|
|
6379
|
-
title: "Parked work",
|
|
6380
|
-
message: waiting.length === 0 ? "No parked work." : [...waiting.map((entry) => `${entry.sessionID === ctx.sessionManager.getSessionId() ? "This session" : entry.sessionID} · ${entry.tier} · ${waitingLabel(entry.resumeAt, now)}`), "Use /adaptive resume or /adaptive cancel in the waiting session."].join("\n")
|
|
6381
|
-
});
|
|
6382
|
-
},
|
|
6383
|
-
doctor: async () => {
|
|
6384
|
-
const checks = await doctorChecks({
|
|
6385
|
-
version: host.version,
|
|
6386
|
-
minimum: host.minimum,
|
|
6387
|
-
testedUpTo: host.testedUpTo,
|
|
6388
|
-
registry: ctx.modelRegistry,
|
|
6389
|
-
settings: () => readSettings(host.settings),
|
|
6390
|
-
capabilities: sources.capabilities,
|
|
6391
|
-
storage: () => host.settings.update(() => ({
|
|
6392
|
-
next: void 0,
|
|
6393
|
-
result: void 0
|
|
6394
|
-
})),
|
|
6395
|
-
health: async () => (await readSharedHealth(sources.health.file)).filter((record) => record.health !== "healthy").length,
|
|
6396
|
-
parked: async () => (await readParked(sources.parking.file)).length
|
|
6397
|
-
});
|
|
6398
|
-
await view.table({
|
|
6399
|
-
title: "Adaptive doctor",
|
|
6400
|
-
lines: doctorLines(checks)
|
|
6401
|
-
});
|
|
6647
|
+
resetHealth: (ref) => resetResourceHealth(sources.health.file, ref),
|
|
6648
|
+
resume: () => parking.resume(ctx),
|
|
6649
|
+
cancel: () => parking.cancel(ctx),
|
|
6650
|
+
tools: async () => {
|
|
6651
|
+
await toolSupportAction(view, host.capabilities, await configured(), await sources.capabilities());
|
|
6402
6652
|
},
|
|
6653
|
+
doctor: () => doctorChecks({
|
|
6654
|
+
version: host.version,
|
|
6655
|
+
minimum: host.minimum,
|
|
6656
|
+
testedUpTo: host.testedUpTo,
|
|
6657
|
+
registry: ctx.modelRegistry,
|
|
6658
|
+
settings: () => readSettings(host.settings),
|
|
6659
|
+
capabilities: sources.capabilities,
|
|
6660
|
+
storage: () => host.settings.update(() => ({
|
|
6661
|
+
next: void 0,
|
|
6662
|
+
result: void 0
|
|
6663
|
+
})),
|
|
6664
|
+
status: api.status
|
|
6665
|
+
}),
|
|
6403
6666
|
preferences: () => preferencesAction(view, sources.preferences),
|
|
6404
6667
|
defaults: () => view.alert({
|
|
6405
6668
|
title: "Use Adaptive for new sessions",
|
|
@@ -6408,12 +6671,7 @@ function adaptiveSurface(pi, sources, host, parking, feedback) {
|
|
|
6408
6671
|
}
|
|
6409
6672
|
});
|
|
6410
6673
|
};
|
|
6411
|
-
registerCommands(pi, {
|
|
6412
|
-
env: environment,
|
|
6413
|
-
resume: (ctx) => parking.resume(ctx),
|
|
6414
|
-
cancel: (ctx) => parking.cancel(ctx),
|
|
6415
|
-
clock: sources.health.clock
|
|
6416
|
-
});
|
|
6674
|
+
registerCommands(pi, { env: environment });
|
|
6417
6675
|
return { async onboard(ctx) {
|
|
6418
6676
|
const env = await environment(ctx);
|
|
6419
6677
|
await runOnboarding({
|
|
@@ -6795,9 +7053,10 @@ async function acquire(lock, clock) {
|
|
|
6795
7053
|
} catch (error) {
|
|
6796
7054
|
if (errorCode(error) !== "EEXIST") throw error;
|
|
6797
7055
|
}
|
|
7056
|
+
const seen = await readFile(lock, "utf8").catch(() => void 0);
|
|
6798
7057
|
const held = await stat(lock).catch(() => void 0);
|
|
6799
|
-
if (held !== void 0 && clock() - held.mtimeMs > STALE_LOCK_MS) {
|
|
6800
|
-
await rm(lock, { force: true });
|
|
7058
|
+
if (seen !== void 0 && held !== void 0 && clock() - held.mtimeMs > STALE_LOCK_MS) {
|
|
7059
|
+
if (await readFile(lock, "utf8").catch(() => void 0) === seen) await rm(lock, { force: true });
|
|
6801
7060
|
continue;
|
|
6802
7061
|
}
|
|
6803
7062
|
if (clock() >= deadline) throw new Error(`Adaptive state: ${lock} is held by another Pi process`);
|
|
@@ -6881,6 +7140,7 @@ function adaptive(pi) {
|
|
|
6881
7140
|
capabilities: () => loadToolCapabilities(join(getAgentDir(), "adaptive", "capabilities.json"), (file) => readFile(file, "utf8"))
|
|
6882
7141
|
}, {
|
|
6883
7142
|
settings: jsonFile(path),
|
|
7143
|
+
capabilities: jsonFile(join(getAgentDir(), "adaptive", "capabilities.json")),
|
|
6884
7144
|
version: VERSION,
|
|
6885
7145
|
minimum,
|
|
6886
7146
|
testedUpTo
|