bareloop 0.23.0 → 0.25.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/CHANGELOG.md +218 -0
- package/bareloop.context.md +233 -19
- package/package.json +1 -1
- package/src/authorflow.js +17 -7
- package/src/authoring.js +32 -0
- package/src/authorjob.js +108 -36
- package/src/calibrate.js +33 -12
- package/src/cardauthor.js +23 -16
- package/src/cli.js +36 -5
- package/src/declaredclose.js +17 -10
- package/src/index.js +17 -5
- package/src/job.js +58 -3
- package/src/judged.js +132 -14
- package/src/kinds.js +84 -13
- package/src/planrun.js +14 -2
- package/src/providers.js +143 -18
- package/src/run.js +9 -4
- package/src/source.js +849 -0
- package/src/validate.js +19 -0
- package/types/authoring.d.ts +30 -0
- package/types/authorjob.d.ts +10 -6
- package/types/calibrate.d.ts +9 -2
- package/types/cardauthor.d.ts +13 -11
- package/types/cli.d.ts +2 -1
- package/types/declaredclose.d.ts +2 -0
- package/types/index.d.ts +2 -1
- package/types/job.d.ts +22 -1
- package/types/judged.d.ts +123 -14
- package/types/kinds.d.ts +66 -3
- package/types/planrun.d.ts +10 -2
- package/types/providers.d.ts +73 -11
- package/types/run.d.ts +9 -3
- package/types/source.d.ts +206 -0
- package/types/validate.d.ts +7 -0
package/src/providers.js
CHANGED
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
// models; it is exactly the two provider identities the PROVIDERS menu
|
|
13
13
|
// (`src/job.js`) admits.
|
|
14
14
|
//
|
|
15
|
-
// What this module is NOT: it never
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// provider
|
|
15
|
+
// What this module is NOT: it never RESOLVES a judge identity itself (that
|
|
16
|
+
// stays arbiter territory, `resolveJudge`/`resolveJobJudge` in
|
|
17
|
+
// src/judged.js — the spec's signed `judge` override, else the job's own
|
|
18
|
+
// worker provider and model, PRD item 32) — it only CONSTRUCTS the judge
|
|
19
|
+
// provider object from an identity the caller already resolved
|
|
20
|
+
// (`buildRunnerProviders`, below); it never prices a round (F113: rates
|
|
21
|
+
// passthrough is dead), and never decides a budget.
|
|
20
22
|
|
|
21
23
|
import { createRequire } from 'node:module';
|
|
22
24
|
|
|
23
25
|
const require = createRequire(import.meta.url);
|
|
24
|
-
const { AnthropicProvider, OpenAIProvider } = require('bare-agent/providers');
|
|
26
|
+
const { AnthropicProvider, OpenAIProvider, GeminiProvider } = require('bare-agent/providers');
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Today's anthropic-api tier table — EXACTLY the `DEFAULT_TIER_MODELS` both
|
|
@@ -49,6 +51,38 @@ export const OPENAI_TIER_MODELS = Object.freeze({
|
|
|
49
51
|
haiku: 'deepseek-chat',
|
|
50
52
|
});
|
|
51
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Gemini (PRD item 31.3, hamr 2026-09-09: "anthropic, openai, gemini drop
|
|
56
|
+
* ollama for now"). Two real tiers, unlike DeepSeek's single model: `flash` is
|
|
57
|
+
* the cheap tier by design, so `haiku` maps to it honestly rather than
|
|
58
|
+
* repeating one id.
|
|
59
|
+
*
|
|
60
|
+
* ADMITTED-PENDING-PROBE. The probe rule stands (PRD item 28 ruling (d),
|
|
61
|
+
* 2026-09-09): no endpoint is in the menu without its own clean paid probe,
|
|
62
|
+
* and `gemini-api` has ZERO runs. `anthropic-api` is the original (~167
|
|
63
|
+
* archived runs) and `openai-api` was probed green through the shipped runner
|
|
64
|
+
* (run mtu12vks, $1.64/$4, F157); gemini owes that fire and has not paid it.
|
|
65
|
+
* `PROBE_STATUS` below is the machine-readable form of that debt — nothing in
|
|
66
|
+
* here silently pretends the provider is proven.
|
|
67
|
+
*/
|
|
68
|
+
export const GEMINI_TIER_MODELS = Object.freeze({
|
|
69
|
+
sonnet: 'gemini-2.5-pro',
|
|
70
|
+
haiku: 'gemini-2.5-flash',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Which admitted providers have paid for their own end-to-end probe, and which
|
|
75
|
+
* have not. Read by the runner to print a loud marker; never read to REFUSE —
|
|
76
|
+
* hamr admitted gemini to the menu, and a table that quietly withheld it would
|
|
77
|
+
* be a second, invisible ruling. An unprobed provider runs, and says so.
|
|
78
|
+
* @type {Readonly<Record<string, {probed: boolean, evidence: string}>>}
|
|
79
|
+
*/
|
|
80
|
+
export const PROBE_STATUS = Object.freeze({
|
|
81
|
+
'anthropic-api': Object.freeze({ probed: true, evidence: 'the original surface — ~167 archived runs' }),
|
|
82
|
+
'openai-api': Object.freeze({ probed: true, evidence: 'run mtu12vks, green through scripts/run-u.mjs, $1.64/$4 (F157)' }),
|
|
83
|
+
'gemini-api': Object.freeze({ probed: false, evidence: 'ZERO runs — admitted to the menu, owes its own paid probe (PRD item 31.3)' }),
|
|
84
|
+
});
|
|
85
|
+
|
|
52
86
|
/**
|
|
53
87
|
* Per-MODEL request-key gating (F149, live-measured 2026-09-09): DeepSeek
|
|
54
88
|
* silently IGNORES the modern `max_completion_tokens` key — asked for 64
|
|
@@ -73,15 +107,44 @@ const OPENAI_MODEL_OPTIONS = Object.freeze({
|
|
|
73
107
|
* @property {new (options: any) => any} ctor the bare-agent provider class
|
|
74
108
|
* @property {string} envKey the environment variable this provider reads its key from
|
|
75
109
|
* @property {Readonly<Record<string, string>>} tiers tier name -> model id
|
|
76
|
-
* @property {
|
|
110
|
+
* @property {string} endpointKey the CONSTRUCTOR OPTION NAME this provider reads its endpoint from — NOT assumed to be `baseUrl` (see below)
|
|
111
|
+
* @property {(model: string|undefined) => Record<string, any>} paramsFor per-model request-key/param gating, applied on TOP of {exposeErrorBody:true, apiKey, model, <endpointKey>?}
|
|
77
112
|
*/
|
|
78
113
|
|
|
114
|
+
// ── WHY `endpointKey` IS A FIELD AND NOT THE LITERAL `baseUrl` ──────────────
|
|
115
|
+
//
|
|
116
|
+
// hamr's framing for this item was "api + api shape + endpoint" (PRD 31.3), and
|
|
117
|
+
// the endpoint half is the one that does NOT generalize. Read from bare-agent's
|
|
118
|
+
// own constructors, not assumed:
|
|
119
|
+
//
|
|
120
|
+
// AnthropicProvider `baseUrl` (provider-anthropic.js:49)
|
|
121
|
+
// OpenAIProvider `baseUrl` (provider-openai.js:76)
|
|
122
|
+
// GeminiProvider `baseUrl` (provider-gemini.js:47)
|
|
123
|
+
// OllamaProvider `url` (provider-ollama.js:32) <- and NO apiKey at all
|
|
124
|
+
//
|
|
125
|
+
// Every one of these constructors defaults its endpoint when the option is
|
|
126
|
+
// absent and none of them validate unknown option names. So passing `baseUrl`
|
|
127
|
+
// to Ollama would not throw: it would be IGNORED, the provider would default to
|
|
128
|
+
// `http://localhost:11434`, and the run would quietly talk to the wrong machine.
|
|
129
|
+
// That is the F149 class exactly — DeepSeek silently ignoring
|
|
130
|
+
// `max_completion_tokens` and returning 665 tokens for a 64-token cap — where a
|
|
131
|
+
// silently-dropped option produces a plausible-looking run against a
|
|
132
|
+
// configuration nobody chose.
|
|
133
|
+
//
|
|
134
|
+
// Ollama is NOT admitted today (hamr, 2026-09-09: "drop ollama for now"), so
|
|
135
|
+
// this field currently reads `baseUrl` for all three entries and its value is
|
|
136
|
+
// never exercised against a second spelling. It is here anyway, because the
|
|
137
|
+
// alternative is a hardcoded `baseUrl` that is correct for exactly as long as
|
|
138
|
+
// the menu holds only http-shaped API providers, and the FIRST thing that
|
|
139
|
+
// changes when Ollama is admitted is the one line nobody would think to look at.
|
|
140
|
+
|
|
79
141
|
/** @type {Readonly<Record<string, ProviderTableEntry>>} */
|
|
80
142
|
const PROVIDER_TABLE = Object.freeze({
|
|
81
143
|
'anthropic-api': Object.freeze({
|
|
82
144
|
ctor: AnthropicProvider,
|
|
83
145
|
envKey: 'ANTHROPIC_API_KEY',
|
|
84
146
|
tiers: ANTHROPIC_TIER_MODELS,
|
|
147
|
+
endpointKey: 'baseUrl',
|
|
85
148
|
// Anthropic has only ever had one request key for the output cap
|
|
86
149
|
// (`max_tokens`) — nothing to gate here, ever (unlike OpenAI-shaped
|
|
87
150
|
// backends, which fork on `max_tokens` vs `max_completion_tokens`).
|
|
@@ -91,8 +154,19 @@ const PROVIDER_TABLE = Object.freeze({
|
|
|
91
154
|
ctor: OpenAIProvider,
|
|
92
155
|
envKey: 'OPENAI_API_KEY',
|
|
93
156
|
tiers: OPENAI_TIER_MODELS,
|
|
157
|
+
endpointKey: 'baseUrl',
|
|
94
158
|
paramsFor: (model) => OPENAI_MODEL_OPTIONS[/** @type {string} */ (model)] ?? {},
|
|
95
159
|
}),
|
|
160
|
+
'gemini-api': Object.freeze({
|
|
161
|
+
ctor: GeminiProvider,
|
|
162
|
+
envKey: 'GEMINI_API_KEY',
|
|
163
|
+
tiers: GEMINI_TIER_MODELS,
|
|
164
|
+
endpointKey: 'baseUrl',
|
|
165
|
+
// Gemini carries no `max_tokens`/`max_completion_tokens` fork of its own —
|
|
166
|
+
// bare-agent's GeminiProvider maps the cap internally. Nothing to gate, and
|
|
167
|
+
// an empty object is the honest statement of that, not an omission.
|
|
168
|
+
paramsFor: () => ({}),
|
|
169
|
+
}),
|
|
96
170
|
});
|
|
97
171
|
|
|
98
172
|
/**
|
|
@@ -130,7 +204,13 @@ export function makeProvider(providerName, { apiKey, model, baseUrl } = {}) {
|
|
|
130
204
|
exposeErrorBody: true,
|
|
131
205
|
apiKey,
|
|
132
206
|
model,
|
|
133
|
-
|
|
207
|
+
// the endpoint goes in under THIS PROVIDER'S OWN OPTION NAME, never a
|
|
208
|
+
// hardcoded `baseUrl` (PRD item 31.3). The spec field stays `baseUrl` for
|
|
209
|
+
// everyone — one name for the person writing the job — and the translation
|
|
210
|
+
// to the constructor's spelling happens here, once. A provider whose option
|
|
211
|
+
// is spelled differently would otherwise IGNORE the endpoint silently and
|
|
212
|
+
// run against its own default; see the note above the table.
|
|
213
|
+
...(baseUrl !== undefined ? { [entry.endpointKey]: baseUrl } : {}),
|
|
134
214
|
...entry.paramsFor(model),
|
|
135
215
|
});
|
|
136
216
|
}
|
|
@@ -144,24 +224,33 @@ export function makeProvider(providerName, { apiKey, model, baseUrl } = {}) {
|
|
|
144
224
|
* before this factory existed — that decision stays where each runner's own
|
|
145
225
|
* `--model` flag/spec precedence lives, not duplicated in here).
|
|
146
226
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
227
|
+
* THE JUDGE IS NO LONGER PINNED TO ANTHROPIC (PRD item 32.2, hamr 2026-09-10:
|
|
228
|
+
* *"what i care about is that judge becomes llm agnostic and not set to one
|
|
229
|
+
* model or provider"*). It is constructed through this same factory, from a
|
|
230
|
+
* `judgeProviderName`/`judgeModel` pair the CALLER resolved (`resolveJudge`,
|
|
231
|
+
* src/judged.js — the spec's signed `judge` override, else the job's own worker
|
|
232
|
+
* provider and model). Item 28's pin, and item 28 part (2)'s deferral of a
|
|
233
|
+
* signed `judge:{provider,model}` field, both close here.
|
|
234
|
+
*
|
|
235
|
+
* `judgeApiKey` stays a SEPARATE argument rather than being derived: the judge
|
|
236
|
+
* may legitimately be a different provider from the worker, and reading its key
|
|
237
|
+
* for it would put a second env-var reader in a module whose whole job is to
|
|
238
|
+
* have exactly one. The caller reads the resolved judge provider's own `envKey`
|
|
239
|
+
* and hands the key in — the same contract the worker key already has here.
|
|
154
240
|
* @param {object} args
|
|
155
241
|
* @param {string} args.providerName spec.provider (or the anthropic-api default)
|
|
156
242
|
* @param {string|undefined} args.apiKey the WORKER's key, read from the provider's own `envKey`
|
|
157
243
|
* @param {string} args.model the already-resolved worker model id
|
|
158
244
|
* @param {Readonly<Record<string, string>>} args.tierModels the already-resolved tier -> model map (spec override folded in by the caller)
|
|
159
245
|
* @param {string|undefined} [args.baseUrl] spec.baseUrl, if any
|
|
160
|
-
* @param {string|undefined} args.judgeApiKey the judge's own key
|
|
161
|
-
* @param {string} args.judgeModel
|
|
246
|
+
* @param {string|undefined} args.judgeApiKey the judge's own key, read by the caller from the RESOLVED judge provider's `envKey`
|
|
247
|
+
* @param {string} args.judgeModel the resolved judge model id
|
|
248
|
+
* @param {string} [args.judgeProviderName] the resolved judge provider name; defaults to `providerName` (the job's own worker provider), which is `resolveJudge`'s own default
|
|
249
|
+
* @param {string|undefined} [args.judgeBaseUrl] the judge provider's endpoint, when it takes one
|
|
162
250
|
*/
|
|
163
251
|
export function buildRunnerProviders({
|
|
164
252
|
providerName, apiKey, model, tierModels, baseUrl, judgeApiKey, judgeModel,
|
|
253
|
+
judgeProviderName = providerName, judgeBaseUrl = undefined,
|
|
165
254
|
}) {
|
|
166
255
|
const provider = makeProvider(providerName, { apiKey, model, baseUrl });
|
|
167
256
|
/** @type {Record<string, any>} */
|
|
@@ -171,6 +260,42 @@ export function buildRunnerProviders({
|
|
|
171
260
|
? provider
|
|
172
261
|
: makeProvider(providerName, { apiKey, model: tierModels[tier], baseUrl })
|
|
173
262
|
));
|
|
174
|
-
|
|
263
|
+
// the judge goes through the SAME table as the worker — one place names a
|
|
264
|
+
// provider identity, which is the entire reason this factory exists. Reusing
|
|
265
|
+
// the worker instance when the identity matches exactly is not an optimisation
|
|
266
|
+
// for its own sake: a second instance against the same endpoint/model is a
|
|
267
|
+
// second prompt-cache prefix, and cache writes are ~41% of the bill.
|
|
268
|
+
const judgeProvider = (judgeProviderName === providerName && judgeModel === model && judgeApiKey === apiKey
|
|
269
|
+
&& judgeBaseUrl === baseUrl)
|
|
270
|
+
? provider
|
|
271
|
+
: makeProvider(judgeProviderName, { apiKey: judgeApiKey, model: judgeModel, baseUrl: judgeBaseUrl });
|
|
175
272
|
return { provider, providerFor, judgeProvider };
|
|
176
273
|
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The launch-time warning lines for an ADMITTED-PENDING-PROBE provider, or
|
|
277
|
+
* `null` when the provider has paid for its own end-to-end probe (and for an
|
|
278
|
+
* off-table name — an unknown provider is `resolveProvider`'s named throw to
|
|
279
|
+
* report, never this function's warning to soften).
|
|
280
|
+
*
|
|
281
|
+
* This lives here, next to `PROBE_STATUS`, rather than inline in the runner,
|
|
282
|
+
* for one reason: inline it was UNTESTABLE. `scripts/run-u.mjs` reaches this
|
|
283
|
+
* point only with a real job row (a machine-local patient path) and a real
|
|
284
|
+
* signed spec, so nothing in the suite could reach the branch, and a warning
|
|
285
|
+
* nothing executes drifts exactly the way the green-only prose did (item 31.2).
|
|
286
|
+
*
|
|
287
|
+
* It never REFUSES, and it never returns a shape a caller can mistake for a
|
|
288
|
+
* refusal: hamr admitted `gemini-api` to the menu, and a probe table that
|
|
289
|
+
* quietly withheld it would be a second ruling nobody made.
|
|
290
|
+
* @param {string} providerName
|
|
291
|
+
* @returns {string[]|null} lines to print, in order, or null when there is nothing to say
|
|
292
|
+
*/
|
|
293
|
+
export function probeWarningLines(providerName) {
|
|
294
|
+
const probe = PROBE_STATUS[/** @type {keyof typeof PROBE_STATUS} */ (providerName)];
|
|
295
|
+
if (!probe || probe.probed !== false) return null;
|
|
296
|
+
return [
|
|
297
|
+
`⚠ UNPROVEN PROVIDER — "${providerName}" is on the menu but has never been probed end to end.`,
|
|
298
|
+
` ${probe.evidence}`,
|
|
299
|
+
' It will run. Nothing has shown that it CAN run this job to a close-rendered verdict.',
|
|
300
|
+
];
|
|
301
|
+
}
|
package/src/run.js
CHANGED
|
@@ -95,9 +95,14 @@ async function primitiveSmoke(workdir) {
|
|
|
95
95
|
* metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
|
|
96
96
|
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
97
97
|
* @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
|
|
98
|
+
* @param {string|null} [opts.judgeModel] SOFTGREEN — WHICH model the judge provider drives
|
|
99
|
+
* (PRD item 32.1), resolved by the CALLER (`resolveJudge`, src/judged.js) and passed
|
|
100
|
+
* through untouched. Travels with `judgeProvider`; absent, a judged stage stops as a
|
|
101
|
+
* wiring gap rather than grading under an identity nobody named.
|
|
98
102
|
* @param {any} [opts.judgeProvider] SOFTGREEN — the provider a JUDGED close stage runs its
|
|
99
|
-
* locate call through,
|
|
100
|
-
*
|
|
103
|
+
* locate call through, built by the CALLER from the RESOLVED judge identity (PRD item
|
|
104
|
+
* 32.1: `resolveJudge`/`resolveJobJudge`, src/judged.js — the spec's signed `judge`
|
|
105
|
+
* override, else the job's own worker provider and model), never pinned. Forwarded to the plan
|
|
101
106
|
* flow. It is its own seam and not `provider`/`providerFor`, because the judge tier is
|
|
102
107
|
* never a step knob and never agent-selectable; absent, a judged stage instrument-stops as
|
|
103
108
|
* a wiring gap rather than grading on whatever binding happened to be at hand. Its spend
|
|
@@ -237,7 +242,7 @@ async function primitiveSmoke(workdir) {
|
|
|
237
242
|
* deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
|
|
238
243
|
* unknown in-flight state is not a known-safe resume point.
|
|
239
244
|
*/
|
|
240
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
245
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
241
246
|
// THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
|
|
242
247
|
// before the ledger, before the approval gate, before a byte of the spec is read.
|
|
243
248
|
// An unrecognised spelling throws here at zero cost instead of being coerced by
|
|
@@ -487,7 +492,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
487
492
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
488
493
|
{
|
|
489
494
|
const outcome = await runPlan(job, {
|
|
490
|
-
workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
|
|
495
|
+
workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
|
|
491
496
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
492
497
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
493
498
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|