@miadi/connector 0.1.12 → 0.1.14
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 +15 -3
- package/dist/approve.d.ts +8 -1
- package/dist/approve.js +39 -7
- package/dist/decisions.d.ts +4 -2
- package/dist/decisions.js +18 -9
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/mission-status.d.ts +121 -15
- package/dist/mission-status.js +199 -27
- package/dist/model-preflight.d.ts +144 -0
- package/dist/model-preflight.js +323 -0
- package/dist/providers.d.ts +29 -3
- package/dist/providers.js +32 -11
- package/dist/queue-state.js +2 -2
- package/dist/records.d.ts +13 -4
- package/dist/records.js +11 -3
- package/dist/request-state.d.ts +9 -1
- package/dist/request-state.js +37 -1
- package/dist/revisions.d.ts +110 -0
- package/dist/revisions.js +245 -0
- package/dist/server.d.ts +7 -3
- package/dist/server.js +4 -1
- package/dist/tools.d.ts +13 -2
- package/dist/tools.js +299 -22
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission model-choice preflight (jgwill/Miadi#913 C): a typed, versioned,
|
|
3
|
+
* read-only answer to "which provider and model fits this mission's task, and
|
|
4
|
+
* how do we know?" — never an authorization.
|
|
5
|
+
*
|
|
6
|
+
* - Policy is deterministic and decided here, not by Jev: the approved provider
|
|
7
|
+
* is kept (no switching without a person), Claude Code only when named, an
|
|
8
|
+
* uninstalled or sign-in-less provider is ineligible, and a reported launch on
|
|
9
|
+
* another provider fails closed as provider_mismatch before anything is asked.
|
|
10
|
+
* - Jev is consulted only when the caller asks (consult_jev) and the connector's
|
|
11
|
+
* environment holds a Jev key. The state sent holds the task type, the
|
|
12
|
+
* criticality and the candidates: no task text, mission id, repository name or
|
|
13
|
+
* source. Its answer is advisory: below the confidence floor, choosing an
|
|
14
|
+
* ineligible option, or failing the answer contract, the preflight abstains.
|
|
15
|
+
* - Every answer says where its choice came from: jev, static_fallback or
|
|
16
|
+
* synthetic_fixture, and Jev's confidence is marked uncalibrated: no calibration
|
|
17
|
+
* set exists yet for this question.
|
|
18
|
+
*
|
|
19
|
+
* The request and answer checks follow lane-check's decider (smcraft
|
|
20
|
+
* examples/decision-gate/lane-check/lib/decider.mjs, revision R9), reduced to
|
|
21
|
+
* the choice questions asked here.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { PROVIDERS } from "./providers.js";
|
|
28
|
+
export const PREFLIGHT_CONTRACT = "miadi.mission_model_preflight";
|
|
29
|
+
export const PREFLIGHT_VERSION = 1;
|
|
30
|
+
/** Below this, a Jev choice is not acted on (the ≥0.8 practice of jgwill/Miadi#770). */
|
|
31
|
+
export const JEV_CONFIDENCE_FLOOR = 0.8;
|
|
32
|
+
export const JEV_MODEL = "jev-latest";
|
|
33
|
+
const SUM_TOLERANCE = 0.025;
|
|
34
|
+
export const TASK_TYPES = ["file_relevance", "architecture_refactor", "implementation", "review"];
|
|
35
|
+
export const CRITICALITIES = ["normal", "security_critical"];
|
|
36
|
+
export const REVIEW_TIERS = ["standard", "stronger_reviewer", "human"];
|
|
37
|
+
// ── candidates, by deterministic policy ──
|
|
38
|
+
/** The model a provider's own config names, when it names one. Only that key is read. */
|
|
39
|
+
export function configuredModel(provider, home = homedir()) {
|
|
40
|
+
try {
|
|
41
|
+
if (provider === "codex") {
|
|
42
|
+
const line = readFileSync(join(home, ".codex", "config.toml"), "utf8").split("\n").find((l) => /^\s*model\s*=/.test(l));
|
|
43
|
+
return line ? (/=\s*"([^"]+)"/.exec(line)?.[1] ?? null) : null;
|
|
44
|
+
}
|
|
45
|
+
if (provider === "claude") {
|
|
46
|
+
const model = JSON.parse(readFileSync(join(home, ".claude", "settings.json"), "utf8"))?.model;
|
|
47
|
+
return typeof model === "string" && model ? model : null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
export function candidatesFor(found, approved, modelOf = (p) => configuredModel(p), basis = "approved") {
|
|
56
|
+
return PROVIDERS.map((spec) => {
|
|
57
|
+
const a = found.find((p) => p.id === spec.id);
|
|
58
|
+
const installed = Boolean(a?.installed);
|
|
59
|
+
const auth = a?.auth_evidence === "found" ? "config_dir_only" : "none";
|
|
60
|
+
const model = installed ? modelOf(spec.id) : null;
|
|
61
|
+
let eligible = true;
|
|
62
|
+
let reason = "Installed, with sign-in evidence (config directory only; quota unknown).";
|
|
63
|
+
if (!installed)
|
|
64
|
+
[eligible, reason] = [false, "Not installed on the connector host."];
|
|
65
|
+
else if (auth === "none")
|
|
66
|
+
[eligible, reason] = [false, "No sign-in evidence on this host."];
|
|
67
|
+
else if (approved && spec.id !== approved)
|
|
68
|
+
[eligible, reason] = [false, `Not the ${basis} provider (${approved}); switching needs a person's approval.`];
|
|
69
|
+
else if (!approved && !spec.selectable_by_default)
|
|
70
|
+
[eligible, reason] = [false, `${spec.name} runs only when a request names it.`];
|
|
71
|
+
return {
|
|
72
|
+
option: model ? `${spec.id}:${model}` : spec.id,
|
|
73
|
+
provider: spec.id,
|
|
74
|
+
model_id: model,
|
|
75
|
+
model_id_source: model ? "configured_default" : "unknown",
|
|
76
|
+
installed,
|
|
77
|
+
...(a?.version ? { version: a.version } : {}),
|
|
78
|
+
auth,
|
|
79
|
+
quota: "unknown",
|
|
80
|
+
eligible,
|
|
81
|
+
policy_reason: reason,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** The least review a task gets, whatever any decider says. Jev may raise it, never lower it. */
|
|
86
|
+
export function reviewFloor(task, criticality) {
|
|
87
|
+
return criticality === "security_critical" || task === "architecture_refactor" ? "stronger_reviewer" : "standard";
|
|
88
|
+
}
|
|
89
|
+
const tierRank = (t) => REVIEW_TIERS.indexOf(t);
|
|
90
|
+
// ── the Jev request, with nothing private in it ──
|
|
91
|
+
export function decisionState(task, criticality, candidates) {
|
|
92
|
+
return {
|
|
93
|
+
task: { type: task, criticality },
|
|
94
|
+
candidates: candidates
|
|
95
|
+
.filter((c) => c.installed)
|
|
96
|
+
.map((c) => ({
|
|
97
|
+
option: c.option,
|
|
98
|
+
provider: c.provider,
|
|
99
|
+
model_id: c.model_id,
|
|
100
|
+
model_id_source: c.model_id_source,
|
|
101
|
+
...(c.capability_note ? { capability_note: c.capability_note } : {}),
|
|
102
|
+
auth: c.auth,
|
|
103
|
+
quota: c.quota,
|
|
104
|
+
eligible: c.eligible,
|
|
105
|
+
})),
|
|
106
|
+
policy: "Only an option with eligible true may be chosen. Choose abstain when no eligible option fits the task with the evidence given.",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function hashState(state) {
|
|
110
|
+
return createHash("sha256").update(JSON.stringify(state)).digest("hex");
|
|
111
|
+
}
|
|
112
|
+
export function buildJevRequest(state, candidates, model = JEV_MODEL) {
|
|
113
|
+
const fit = {};
|
|
114
|
+
for (const c of candidates.filter((x) => x.eligible)) {
|
|
115
|
+
const model = c.model_id ? ` with model ${c.model_id}${c.model_id_source === "configured_default" ? " (its configured default)" : ""}` : ", model unknown";
|
|
116
|
+
fit[c.option] = `${c.provider}${model}${c.capability_note ? ` (${c.capability_note})` : ""}; sign-in evidence is a config directory only; quota unknown.`;
|
|
117
|
+
}
|
|
118
|
+
fit.abstain = "No eligible option fits this task type and criticality on the evidence given.";
|
|
119
|
+
return {
|
|
120
|
+
state,
|
|
121
|
+
model,
|
|
122
|
+
questions: {
|
|
123
|
+
fit: {
|
|
124
|
+
type: "choice",
|
|
125
|
+
instructions: "Which option in `candidates` best fits a task of type `task.type` with criticality `task.criticality`? A file-relevance task needs high recall; an architecture or refactor task needs a strong model.",
|
|
126
|
+
criteria: fit,
|
|
127
|
+
},
|
|
128
|
+
review_tier: {
|
|
129
|
+
type: "choice",
|
|
130
|
+
instructions: "What review should the result of this task get before it is merged?",
|
|
131
|
+
criteria: {
|
|
132
|
+
standard: "One reviewer pass is enough.",
|
|
133
|
+
stronger_reviewer: "A stronger model or reviewer must check it: security-relevant files or architecture are involved.",
|
|
134
|
+
human: "A person must review it before anything is merged.",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const isUnit = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 && x <= 1;
|
|
141
|
+
/** A choice answer against its question: the failures, empty when it holds. */
|
|
142
|
+
export function checkChoice(criteria, answer) {
|
|
143
|
+
const out = [];
|
|
144
|
+
if (!answer || typeof answer !== "object")
|
|
145
|
+
return ["no answer"];
|
|
146
|
+
if (answer.type !== "choice")
|
|
147
|
+
out.push(`answered a ${answer.type}, asked a choice`);
|
|
148
|
+
if (!isUnit(answer.confidence))
|
|
149
|
+
out.push(`confidence ${answer.confidence} is not within 0 and 1`);
|
|
150
|
+
const keys = Object.keys(criteria);
|
|
151
|
+
const probs = answer.probabilities;
|
|
152
|
+
if (!probs || typeof probs !== "object")
|
|
153
|
+
return [...out, "no probabilities"];
|
|
154
|
+
const outOfRange = Object.keys(probs).filter((k) => !isUnit(probs[k]));
|
|
155
|
+
if (outOfRange.length)
|
|
156
|
+
out.push(`probabilities outside 0 and 1: ${outOfRange.join(", ")}`);
|
|
157
|
+
const missing = keys.filter((k) => !(k in probs));
|
|
158
|
+
const extra = Object.keys(probs).filter((k) => !keys.includes(k));
|
|
159
|
+
if (missing.length || extra.length)
|
|
160
|
+
out.push("probabilities do not cover exactly the declared options");
|
|
161
|
+
const sum = keys.reduce((acc, k) => acc + (isUnit(probs[k]) ? probs[k] : 0), 0);
|
|
162
|
+
if (!missing.length && Math.abs(sum - 1) > SUM_TOLERANCE)
|
|
163
|
+
out.push(`probabilities sum to ${sum.toFixed(3)}`);
|
|
164
|
+
if (!keys.includes(answer.choice))
|
|
165
|
+
out.push(`chose ${answer.choice}, not a declared option`);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
// ── the adapter to the real service ──
|
|
169
|
+
/** One call to Jev, never resent: a lost answer may already have been charged. */
|
|
170
|
+
export function jevAdapter(config, fetchImpl = fetch, timeoutMs = 60_000) {
|
|
171
|
+
return {
|
|
172
|
+
source: "jev",
|
|
173
|
+
model: JEV_MODEL,
|
|
174
|
+
async decide(request) {
|
|
175
|
+
const t0 = Date.now();
|
|
176
|
+
const res = await fetchImpl(config.url, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json", accept: "application/json" },
|
|
179
|
+
body: JSON.stringify(request),
|
|
180
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
181
|
+
});
|
|
182
|
+
const latency_ms = Date.now() - t0;
|
|
183
|
+
const billing = {};
|
|
184
|
+
res.headers.forEach((v, k) => {
|
|
185
|
+
if (k.startsWith("x-jev-"))
|
|
186
|
+
billing[k.slice(6).replace(/-/g, "_")] = v;
|
|
187
|
+
});
|
|
188
|
+
if (!res.ok)
|
|
189
|
+
throw new Error(`Jev answered ${res.status}${billing.run_id ? ` [run ${billing.run_id}]` : ""}`);
|
|
190
|
+
const body = await res.json();
|
|
191
|
+
return { answers: body?.answers ?? {}, billing: { ...(body?.billing ?? {}), ...billing }, latency_ms };
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export const ACTIVATION_PLAN = [
|
|
196
|
+
"Put JEV_AI_API_KEY (or TYPESAFE_API_KEY) in an environment file readable only by the connector's user, never in the repository or the unit file.",
|
|
197
|
+
"Add that file to miadi-connector.service as EnvironmentFile and restart the service.",
|
|
198
|
+
"Call provider_capabilities: router.state must be reachable (GET /v1/models, which runs no inference and is not charged).",
|
|
199
|
+
"Call mission_model_preflight with consult_jev true on one mission. Each such call is charged by Jev; its billing headers come back in evidence.jev_billing.",
|
|
200
|
+
];
|
|
201
|
+
export async function modelPreflight(input) {
|
|
202
|
+
const { task, criticality, candidates, approved_provider } = input;
|
|
203
|
+
const state = decisionState(task, criticality, candidates);
|
|
204
|
+
const floor = reviewFloor(task, criticality);
|
|
205
|
+
const eligible = candidates.filter((c) => c.eligible);
|
|
206
|
+
const base = {
|
|
207
|
+
contract: PREFLIGHT_CONTRACT,
|
|
208
|
+
version: PREFLIGHT_VERSION,
|
|
209
|
+
mission: input.mission,
|
|
210
|
+
input_hash: hashState(state),
|
|
211
|
+
task: { type: task, criticality },
|
|
212
|
+
approved_provider,
|
|
213
|
+
provider_basis: input.provider_basis ?? "approved",
|
|
214
|
+
candidates,
|
|
215
|
+
evidence: { model_cost: "unknown", quota: "unknown" },
|
|
216
|
+
policy: {
|
|
217
|
+
deterministic: true,
|
|
218
|
+
authorizes: false,
|
|
219
|
+
note: "A recommendation authorizes nothing. Permissions, provider switching, deployment and spending stay with the approved request and a person's decision.",
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const launch = input.launch
|
|
223
|
+
? { approved_provider, actual: { ...input.launch, source: "reported" }, mismatch: approved_provider ? input.launch.provider !== approved_provider : null }
|
|
224
|
+
: { approved_provider, actual: "unverified", mismatch: null };
|
|
225
|
+
// Fail closed before anything is asked or spent.
|
|
226
|
+
if (launch.mismatch) {
|
|
227
|
+
return {
|
|
228
|
+
...base,
|
|
229
|
+
state: "provider_mismatch",
|
|
230
|
+
decider: { source: "static_fallback", router: "not_requested", calibration: "not_applicable", derivation: "deterministic policy: the reported launch is not the approved provider" },
|
|
231
|
+
recommendation: {
|
|
232
|
+
confidence: null,
|
|
233
|
+
review_tier: floor,
|
|
234
|
+
explanation: `${(input.provider_basis ?? "approved") === "approved" ? "Approved" : "Requested (not approved)"} ${approved_provider}, reported running on ${input.launch.provider}. Nothing proceeds on an unapproved provider; a person re-approves or the launch is corrected.`,
|
|
235
|
+
abstain_reasons: ["provider_mismatch"],
|
|
236
|
+
},
|
|
237
|
+
launch,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const fallback = (router, reasons, detail, extra = {}) => {
|
|
241
|
+
const pick = approved_provider ? eligible.find((c) => c.provider === approved_provider) : eligible[0];
|
|
242
|
+
const abstain = [...reasons, ...(pick ? [] : [approved_provider ? "approved_provider_ineligible" : "no_eligible_provider"])];
|
|
243
|
+
return {
|
|
244
|
+
...base,
|
|
245
|
+
state: pick && !reasons.length ? "recommended" : "abstained",
|
|
246
|
+
decider: {
|
|
247
|
+
source: "static_fallback",
|
|
248
|
+
router,
|
|
249
|
+
calibration: "not_applicable",
|
|
250
|
+
derivation: approved_provider ? `deterministic policy: the ${input.provider_basis ?? "approved"} provider and its configured default model` : "deterministic policy: preference order codex, pi, copilot; claude only when named",
|
|
251
|
+
...(detail ? { detail } : {}),
|
|
252
|
+
},
|
|
253
|
+
recommendation: {
|
|
254
|
+
...(pick && !reasons.length ? { provider: pick.provider, model_id: pick.model_id } : {}),
|
|
255
|
+
confidence: null,
|
|
256
|
+
review_tier: floor,
|
|
257
|
+
explanation: pick
|
|
258
|
+
? reasons.length
|
|
259
|
+
? `The static policy would keep ${pick.option}, but the preflight abstains: ${reasons.join(", ")}.`
|
|
260
|
+
: `${pick.option} by static policy. No task-aware choice was made; its model is the provider's configured default.`
|
|
261
|
+
: `No eligible option: ${candidates.map((c) => `${c.provider} (${c.policy_reason})`).join("; ")}`,
|
|
262
|
+
abstain_reasons: pick && !reasons.length ? [] : abstain,
|
|
263
|
+
},
|
|
264
|
+
evidence: { ...base.evidence, ...extra },
|
|
265
|
+
launch,
|
|
266
|
+
...(router === "router_unconfigured" ? { activation_plan: ACTIVATION_PLAN } : {}),
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
if (!input.adapter)
|
|
270
|
+
return fallback(input.routerWhenAbsent ?? "not_requested", []);
|
|
271
|
+
if (!eligible.length)
|
|
272
|
+
return fallback("not_requested", [], "No eligible option, so Jev was not asked.");
|
|
273
|
+
const request = buildJevRequest(state, candidates, input.adapter.model);
|
|
274
|
+
let answer;
|
|
275
|
+
try {
|
|
276
|
+
answer = await input.adapter.decide(request);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return fallback("router_unavailable", ["jev_unavailable"], error instanceof Error ? error.message : String(error));
|
|
280
|
+
}
|
|
281
|
+
const fitQ = request.questions.fit.criteria;
|
|
282
|
+
const tierQ = request.questions.review_tier.criteria;
|
|
283
|
+
const fit = answer.answers.fit;
|
|
284
|
+
const tier = answer.answers.review_tier;
|
|
285
|
+
const failures = [...checkChoice(fitQ, fit).map((f) => `fit: ${f}`), ...checkChoice(tierQ, tier).map((f) => `review_tier: ${f}`)];
|
|
286
|
+
const spent = { ...(answer.latency_ms !== undefined ? { latency_ms: answer.latency_ms } : {}), ...(answer.billing && Object.keys(answer.billing).length ? { jev_billing: answer.billing } : {}) };
|
|
287
|
+
if (failures.length)
|
|
288
|
+
return fallback("invalid_answer", ["jev_invalid_answer"], failures.join("; "), spent);
|
|
289
|
+
const chosen = eligible.find((c) => c.option === fit.choice);
|
|
290
|
+
const reviewTier = tierRank(tier.choice) > tierRank(floor) ? tier.choice : floor;
|
|
291
|
+
const reasons = [];
|
|
292
|
+
if (fit.choice === "abstain")
|
|
293
|
+
reasons.push("jev_abstained");
|
|
294
|
+
else if (!chosen)
|
|
295
|
+
reasons.push("jev_choice_ineligible");
|
|
296
|
+
if (fit.confidence < JEV_CONFIDENCE_FLOOR)
|
|
297
|
+
reasons.push("jev_low_confidence");
|
|
298
|
+
const recommend = !reasons.length && chosen;
|
|
299
|
+
return {
|
|
300
|
+
...base,
|
|
301
|
+
state: recommend ? "recommended" : "abstained",
|
|
302
|
+
decider: {
|
|
303
|
+
source: input.adapter.source,
|
|
304
|
+
router: "answered",
|
|
305
|
+
model: input.adapter.model,
|
|
306
|
+
calibration: "uncalibrated",
|
|
307
|
+
confidence_floor: JEV_CONFIDENCE_FLOOR,
|
|
308
|
+
derivation: "Jev chose among the eligible options; the policy decided eligibility and the review floor, and abstains below the confidence floor",
|
|
309
|
+
},
|
|
310
|
+
recommendation: {
|
|
311
|
+
...(recommend ? { provider: chosen.provider, model_id: chosen.model_id } : {}),
|
|
312
|
+
confidence: fit.confidence,
|
|
313
|
+
probabilities: fit.probabilities,
|
|
314
|
+
review_tier: reviewTier,
|
|
315
|
+
explanation: recommend
|
|
316
|
+
? `${input.adapter.source === "jev" ? "Jev" : "The synthetic fixture"} chose ${chosen.option} at confidence ${fit.confidence} (uncalibrated); review ${reviewTier}${reviewTier !== tier.choice ? " (the policy floor)" : ""}.`
|
|
317
|
+
: `Abstained: ${reasons.join(", ")}. ${input.adapter.source === "jev" ? "Jev" : "The fixture"} answered ${fit.choice} at confidence ${fit.confidence}.`,
|
|
318
|
+
abstain_reasons: reasons,
|
|
319
|
+
},
|
|
320
|
+
evidence: { ...base.evidence, ...spent },
|
|
321
|
+
launch,
|
|
322
|
+
};
|
|
323
|
+
}
|
package/dist/providers.d.ts
CHANGED
|
@@ -42,20 +42,46 @@ export type ProviderResolution = {
|
|
|
42
42
|
* by an explicit request.
|
|
43
43
|
*/
|
|
44
44
|
export declare function resolveProvider(requested: string | undefined, found: ProviderAvailability[]): ProviderResolution;
|
|
45
|
+
/**
|
|
46
|
+
* Where Jev is reached from this host (jgwill/Miadi#913 C1), by the names
|
|
47
|
+
* smcraft's lane-check decider uses (examples/decision-gate/lane-check,
|
|
48
|
+
* jgwill/Miadi#770): JEV_AI_API_KEY at jev-ai.pro, else TYPESAFE_API_KEY at
|
|
49
|
+
* typesafe.ai, each with an optional base override; MIADI_JEV_URL is kept as a
|
|
50
|
+
* base override. Only the connector's own environment is read, never a file.
|
|
51
|
+
* The key stays in the returned object for the adapter; nothing here prints it.
|
|
52
|
+
*/
|
|
53
|
+
export interface JevConfig {
|
|
54
|
+
service: string;
|
|
55
|
+
key_name: string;
|
|
56
|
+
base: string;
|
|
57
|
+
url: string;
|
|
58
|
+
models_url: string;
|
|
59
|
+
/** Never serialised: callers pass the config to the adapter, not to an answer. */
|
|
60
|
+
readonly apiKey: string;
|
|
61
|
+
}
|
|
62
|
+
export declare const JEV_KEY_NAMES: string[];
|
|
63
|
+
export declare function jevConfigOf(env?: NodeJS.ProcessEnv): JevConfig | null;
|
|
45
64
|
export type RouterPreflight = {
|
|
46
65
|
state: "unconfigured";
|
|
47
66
|
note: string;
|
|
48
67
|
} | {
|
|
49
68
|
state: "reachable";
|
|
69
|
+
service: string;
|
|
70
|
+
key_name: string;
|
|
50
71
|
url: string;
|
|
72
|
+
models: string[];
|
|
73
|
+
note: string;
|
|
51
74
|
} | {
|
|
52
75
|
state: "router_unavailable";
|
|
76
|
+
service: string;
|
|
77
|
+
key_name: string;
|
|
53
78
|
url: string;
|
|
54
79
|
message: string;
|
|
55
80
|
};
|
|
56
81
|
/**
|
|
57
|
-
* The read-only Jev
|
|
58
|
-
*
|
|
59
|
-
*
|
|
82
|
+
* The read-only Jev preflight: is a Jev key in the connector's environment, and
|
|
83
|
+
* does Jev list models for it? GET /v1/models runs no inference and is not
|
|
84
|
+
* charged. Reachable is not a decision: a scored choice comes only from
|
|
85
|
+
* mission_model_preflight with consult_jev. No key value is read into an answer.
|
|
60
86
|
*/
|
|
61
87
|
export declare function routerPreflight(env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch): Promise<RouterPreflight>;
|
package/dist/providers.js
CHANGED
|
@@ -95,26 +95,47 @@ export function resolveProvider(requested, found) {
|
|
|
95
95
|
}
|
|
96
96
|
return { state: "provider_unavailable", message: "No agent provider is installed on the connector host.", available: [] };
|
|
97
97
|
}
|
|
98
|
+
const JEV_SERVICES = [
|
|
99
|
+
{ service: "jev-ai.pro", key: "JEV_AI_API_KEY", baseEnv: "JEV_AI_BASE_URL", base: "https://jev-ai.pro/api" },
|
|
100
|
+
{ service: "typesafe.ai", key: "TYPESAFE_API_KEY", baseEnv: "TYPESAFE_BASE_URL", base: "https://api.typesafe.ai" },
|
|
101
|
+
];
|
|
102
|
+
export const JEV_KEY_NAMES = JEV_SERVICES.map((s) => s.key);
|
|
103
|
+
export function jevConfigOf(env = process.env) {
|
|
104
|
+
for (const s of JEV_SERVICES) {
|
|
105
|
+
const apiKey = env[s.key];
|
|
106
|
+
if (!apiKey)
|
|
107
|
+
continue;
|
|
108
|
+
const base = (env[s.baseEnv] || env.MIADI_JEV_URL || s.base).replace(/\/+$/, "");
|
|
109
|
+
const config = { service: s.service, key_name: s.key, base, url: `${base}/v1/systemone`, models_url: `${base}/v1/models` };
|
|
110
|
+
Object.defineProperty(config, "apiKey", { value: apiKey, enumerable: false });
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
98
115
|
/**
|
|
99
|
-
* The read-only Jev
|
|
100
|
-
*
|
|
101
|
-
*
|
|
116
|
+
* The read-only Jev preflight: is a Jev key in the connector's environment, and
|
|
117
|
+
* does Jev list models for it? GET /v1/models runs no inference and is not
|
|
118
|
+
* charged. Reachable is not a decision: a scored choice comes only from
|
|
119
|
+
* mission_model_preflight with consult_jev. No key value is read into an answer.
|
|
102
120
|
*/
|
|
103
121
|
export async function routerPreflight(env = process.env, fetchImpl = fetch) {
|
|
104
|
-
const
|
|
105
|
-
if (!
|
|
122
|
+
const jev = jevConfigOf(env);
|
|
123
|
+
if (!jev) {
|
|
106
124
|
return {
|
|
107
125
|
state: "unconfigured",
|
|
108
|
-
note:
|
|
126
|
+
note: `No ${JEV_KEY_NAMES.join(" or ")} in the connector's environment: model choice falls back to the explicit provider policy and the person's choice, never to a silent default.`,
|
|
109
127
|
};
|
|
110
128
|
}
|
|
129
|
+
const where = { service: jev.service, key_name: jev.key_name, url: jev.base };
|
|
111
130
|
try {
|
|
112
|
-
const res = await fetchImpl(
|
|
113
|
-
if (res.ok)
|
|
114
|
-
return { state: "
|
|
115
|
-
|
|
131
|
+
const res = await fetchImpl(jev.models_url, { headers: { authorization: `Bearer ${jev.apiKey}`, accept: "application/json" }, signal: AbortSignal.timeout(5000) });
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
return { state: "router_unavailable", ...where, message: `Jev answered ${res.status} to the model list; route by the explicit provider policy instead.` };
|
|
134
|
+
const body = await res.json().catch(() => ({}));
|
|
135
|
+
const models = Array.isArray(body?.models) ? body.models.map((m) => (typeof m === "string" ? m : String(m?.id ?? m?.name ?? ""))).filter(Boolean) : [];
|
|
136
|
+
return { state: "reachable", ...where, models, note: "Jev lists models for this key. That is reachability, not a decision." };
|
|
116
137
|
}
|
|
117
138
|
catch (error) {
|
|
118
|
-
return { state: "router_unavailable",
|
|
139
|
+
return { state: "router_unavailable", ...where, message: `Jev is not reachable (${error instanceof Error ? error.message : String(error)}); route by the explicit provider policy instead.` };
|
|
119
140
|
}
|
|
120
141
|
}
|
package/dist/queue-state.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Shared, read-only factory queue evidence for every request-state surface. */
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
export function factoryDir(env = process.env) {
|
|
6
|
-
|
|
5
|
+
// The factory's data folder, beside the connector's own /srv/miadi/connector: never a home directory (jgwill/Miadi#911).
|
|
6
|
+
return env.MIADI_FACTORY_DIR || "/srv/miadi/factory";
|
|
7
7
|
}
|
|
8
8
|
export function runBriefFile(record) {
|
|
9
9
|
return `connector-${record.id.replace(/[^\w-]/g, "-")}.md`;
|
package/dist/records.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestLineage, RequestRevision } from "./revisions.js";
|
|
1
2
|
export declare const CIRCLES: readonly [{
|
|
2
3
|
readonly id: "story";
|
|
3
4
|
readonly glyph: "📖";
|
|
@@ -81,13 +82,21 @@ export interface ConnectorRecord {
|
|
|
81
82
|
supersedes?: string;
|
|
82
83
|
idempotency_key?: string;
|
|
83
84
|
completion_receipt?: Record<string, unknown>;
|
|
85
|
+
/** A revision of a waiting request: this record carries its whole new content (jgwill/Miadi#919). */
|
|
86
|
+
revision?: RequestRevision;
|
|
87
|
+
/** The requests a waiting request adopted as its earlier versions (jgwill/Miadi#919). */
|
|
88
|
+
lineage?: RequestLineage;
|
|
89
|
+
/** Folded only: the version the request is at, counted across its lineage. Absent means 1. */
|
|
90
|
+
version?: number;
|
|
84
91
|
}
|
|
85
92
|
export declare function newRecordId(kind: RecordKind): string;
|
|
86
|
-
/**
|
|
87
|
-
* Fold the ledger: a record superseded by a later one shows the later one's
|
|
88
|
-
* state, approval and receipt, keeping its own id and history.
|
|
89
|
-
*/
|
|
90
93
|
export { projectRequest, APPROVABLE_KINDS, APPROVABLE } from "./request-state.js";
|
|
91
94
|
export { readQueueState, runBriefFile } from "./queue-state.js";
|
|
92
95
|
export type { RequestProjection, RequestStage, RequestStanding, RequestDecision, ProjectionContext } from "./request-state.js";
|
|
96
|
+
/**
|
|
97
|
+
* Fold the ledger: a record superseded by a later one shows the later one's
|
|
98
|
+
* state, approval and receipt, keeping its own id and history. A revision also
|
|
99
|
+
* replaces the content (intention, circle, refs, body) and moves the version;
|
|
100
|
+
* a lineage adds the versions of the requests it adopted (jgwill/Miadi#919).
|
|
101
|
+
*/
|
|
93
102
|
export declare function current(records: ConnectorRecord[]): ConnectorRecord[];
|
package/dist/records.js
CHANGED
|
@@ -73,19 +73,27 @@ export const APPROVAL_STATES = ["awaiting_approval", "approved", "declined", "re
|
|
|
73
73
|
export function newRecordId(kind) {
|
|
74
74
|
return `${kind}:${randomUUID()}`;
|
|
75
75
|
}
|
|
76
|
+
export { projectRequest, APPROVABLE_KINDS, APPROVABLE } from "./request-state.js";
|
|
77
|
+
export { readQueueState, runBriefFile } from "./queue-state.js";
|
|
76
78
|
/**
|
|
77
79
|
* Fold the ledger: a record superseded by a later one shows the later one's
|
|
78
|
-
* state, approval and receipt, keeping its own id and history.
|
|
80
|
+
* state, approval and receipt, keeping its own id and history. A revision also
|
|
81
|
+
* replaces the content (intention, circle, refs, body) and moves the version;
|
|
82
|
+
* a lineage adds the versions of the requests it adopted (jgwill/Miadi#919).
|
|
79
83
|
*/
|
|
80
|
-
export { projectRequest, APPROVABLE_KINDS, APPROVABLE } from "./request-state.js";
|
|
81
|
-
export { readQueueState, runBriefFile } from "./queue-state.js";
|
|
82
84
|
export function current(records) {
|
|
83
85
|
const byId = new Map();
|
|
84
86
|
for (const r of records) {
|
|
85
87
|
if (r.supersedes && byId.has(r.supersedes)) {
|
|
86
88
|
const base = byId.get(r.supersedes);
|
|
89
|
+
const adopted = r.lineage ? r.lineage.adopted.reduce((n, a) => n + (byId.get(a)?.version ?? 1), 0) : 0;
|
|
87
90
|
byId.set(r.supersedes, {
|
|
88
91
|
...base,
|
|
92
|
+
...(r.revision
|
|
93
|
+
? { intention: r.intention, circle: r.circle ?? base.circle, refs: r.refs, body: r.body, version: r.revision.version }
|
|
94
|
+
: r.lineage
|
|
95
|
+
? { version: (base.version ?? 1) + adopted }
|
|
96
|
+
: {}),
|
|
89
97
|
state: r.state ?? base.state,
|
|
90
98
|
approval_state: r.approval_state ?? base.approval_state,
|
|
91
99
|
next_action: r.next_action ?? base.next_action,
|
package/dist/request-state.d.ts
CHANGED
|
@@ -54,6 +54,10 @@ export interface RequestProjection {
|
|
|
54
54
|
outstanding: string[];
|
|
55
55
|
/** Every sentence a surface shows under the label, names and instructions included. */
|
|
56
56
|
lines: string[];
|
|
57
|
+
/** The version the request is at (jgwill/Miadi#919); a decision names it. 1 for a request never revised. */
|
|
58
|
+
version: number;
|
|
59
|
+
/** A request retired as an earlier version of another: the request to decide instead. */
|
|
60
|
+
superseded_by?: string;
|
|
57
61
|
}
|
|
58
62
|
export interface ProjectionContext {
|
|
59
63
|
/** The review service's public address, for receipt_url. Default MIADI_REVIEW_BASE_URL, else the public service. */
|
|
@@ -65,5 +69,9 @@ export interface ProjectionContext {
|
|
|
65
69
|
/** A decision on this request is being recorded now (its run may still be going). */
|
|
66
70
|
recording?: boolean;
|
|
67
71
|
}
|
|
68
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* The projection of an approvable request, or null for a record that is not one. A
|
|
74
|
+
* request retired as an earlier version of another reads as superseded; a revised one
|
|
75
|
+
* says its version, and a request sent back says which version answers it (#919).
|
|
76
|
+
*/
|
|
69
77
|
export declare function projectRequest(record: ConnectorRecord, ctx?: ProjectionContext): RequestProjection | null;
|
package/dist/request-state.js
CHANGED
|
@@ -17,6 +17,7 @@ const CHOICE_WORDS = {
|
|
|
17
17
|
request_revision: "Sent back for revision",
|
|
18
18
|
decline: "Declined",
|
|
19
19
|
covered_by_grant: "Covered by the approved run's steering grant",
|
|
20
|
+
superseded: "Superseded by a later version",
|
|
20
21
|
};
|
|
21
22
|
const OUTCOME_WORDS = {
|
|
22
23
|
queued: "queued for the factory intake",
|
|
@@ -37,8 +38,43 @@ function reviewBaseOf(ctx) {
|
|
|
37
38
|
const base = ctx.reviewBase ?? process.env.MIADI_REVIEW_BASE_URL ?? "https://miadi-review-service.vercel.app";
|
|
38
39
|
return base.replace(/\/$/, "");
|
|
39
40
|
}
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* The projection of an approvable request, or null for a record that is not one. A
|
|
43
|
+
* request retired as an earlier version of another reads as superseded; a revised one
|
|
44
|
+
* says its version, and a request sent back says which version answers it (#919).
|
|
45
|
+
*/
|
|
41
46
|
export function projectRequest(record, ctx = {}) {
|
|
47
|
+
const p = projectCore(record, ctx);
|
|
48
|
+
if (!p)
|
|
49
|
+
return null;
|
|
50
|
+
const version = typeof record.version === "number" && record.version >= 1 ? record.version : 1;
|
|
51
|
+
const receipt = (record.completion_receipt ?? {});
|
|
52
|
+
if (record.approval_state === "declined" && receipt.decision === "superseded") {
|
|
53
|
+
const by = text(receipt.superseded_by);
|
|
54
|
+
const who = text(receipt.decided_by?.name) || "someone";
|
|
55
|
+
const when = text(receipt.decided_at) ? ` at ${text(receipt.decided_at)}` : "";
|
|
56
|
+
const why = text(receipt.reason) ? `: ${text(receipt.reason)}` : "";
|
|
57
|
+
return {
|
|
58
|
+
...p,
|
|
59
|
+
version,
|
|
60
|
+
label: "Superseded by a later version",
|
|
61
|
+
decision: p.decision ? { ...p.decision, choice: "superseded" } : p.decision,
|
|
62
|
+
...(by ? { superseded_by: by } : {}),
|
|
63
|
+
lines: [`Superseded by ${by || "a later request"}, as ${who} asked${when}${why}.`, `Nothing runs from this version. Decide ${by || "the later request"} instead.`],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const lines = [...p.lines];
|
|
67
|
+
if (record.approval_state === "awaiting_approval" && receipt.decision === "request_revision" && receipt.answered_by_version) {
|
|
68
|
+
const who = text(receipt.decided_by?.name) || "someone";
|
|
69
|
+
const asked = text(receipt.instructions).replace(/[.\s]+$/, "");
|
|
70
|
+
const answered = Number(receipt.answered_by_version);
|
|
71
|
+
lines.push(`Sent back for revision by ${who}${asked ? `: ${asked}` : ""}. Version ${answered} ${answered === version ? "answers" : "answered"} it.`);
|
|
72
|
+
}
|
|
73
|
+
if (version > 1)
|
|
74
|
+
lines.unshift(`Version ${version}; the earlier versions stay readable (get_request_history).`);
|
|
75
|
+
return { ...p, version, lines };
|
|
76
|
+
}
|
|
77
|
+
function projectCore(record, ctx = {}) {
|
|
42
78
|
if (!APPROVABLE.has(record.kind))
|
|
43
79
|
return null;
|
|
44
80
|
const receipt = (record.completion_receipt ?? {});
|