bare-agent 0.42.0 → 0.44.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.
Files changed (82) hide show
  1. package/bareagent.context.md +1 -1
  2. package/package.json +8 -3
  3. package/primitives.json +447 -0
  4. package/src/bareguard-adapter.d.ts +6 -0
  5. package/src/bareguard-adapter.js +6 -0
  6. package/src/checkpoint.d.ts +5 -0
  7. package/src/checkpoint.js +5 -0
  8. package/src/circuit-breaker.d.ts +5 -0
  9. package/src/circuit-breaker.js +5 -0
  10. package/src/complexity.d.ts +9 -0
  11. package/src/complexity.js +9 -0
  12. package/src/context-units.d.ts +12 -0
  13. package/src/context-units.js +12 -0
  14. package/src/evaluator.d.ts +9 -1
  15. package/src/evaluator.js +9 -1
  16. package/src/judge-calibration.d.ts +5 -0
  17. package/src/judge-calibration.js +5 -0
  18. package/src/judge.d.ts +5 -0
  19. package/src/judge.js +5 -0
  20. package/src/loop.d.ts +16 -2
  21. package/src/loop.js +29 -12
  22. package/src/mcp-bridge.d.ts +13 -0
  23. package/src/mcp-bridge.js +13 -0
  24. package/src/memory.d.ts +5 -0
  25. package/src/memory.js +5 -0
  26. package/src/planner.d.ts +6 -0
  27. package/src/planner.js +6 -0
  28. package/src/provider-anthropic.d.ts +4 -0
  29. package/src/provider-anthropic.js +4 -0
  30. package/src/provider-clipipe.d.ts +4 -0
  31. package/src/provider-clipipe.js +4 -0
  32. package/src/provider-fallback.d.ts +4 -0
  33. package/src/provider-fallback.js +4 -0
  34. package/src/provider-gemini.d.ts +7 -1
  35. package/src/provider-gemini.js +7 -1
  36. package/src/provider-ollama.d.ts +4 -0
  37. package/src/provider-ollama.js +11 -2
  38. package/src/provider-openai.d.ts +4 -0
  39. package/src/provider-openai.js +31 -3
  40. package/src/provider-toolcalls.d.ts +29 -0
  41. package/src/provider-toolcalls.js +45 -0
  42. package/src/recurse-retrieval.d.ts +22 -0
  43. package/src/recurse-retrieval.js +22 -0
  44. package/src/recurse.d.ts +6 -0
  45. package/src/recurse.js +6 -0
  46. package/src/refine.d.ts +5 -0
  47. package/src/refine.js +5 -0
  48. package/src/remember.d.ts +5 -0
  49. package/src/remember.js +5 -0
  50. package/src/retry.d.ts +8 -1
  51. package/src/retry.js +8 -1
  52. package/src/run-plan.d.ts +5 -0
  53. package/src/run-plan.js +5 -0
  54. package/src/scheduler.d.ts +8 -1
  55. package/src/scheduler.js +8 -1
  56. package/src/skills.d.ts +6 -0
  57. package/src/skills.js +6 -0
  58. package/src/stash.d.ts +5 -0
  59. package/src/stash.js +5 -0
  60. package/src/state.d.ts +8 -1
  61. package/src/state.js +8 -1
  62. package/src/store-jsonfile.d.ts +5 -0
  63. package/src/store-jsonfile.js +5 -0
  64. package/src/store-sqlite.d.ts +5 -0
  65. package/src/store-sqlite.js +5 -0
  66. package/src/stream.d.ts +5 -0
  67. package/src/stream.js +5 -0
  68. package/src/transport-jsonl.d.ts +4 -0
  69. package/src/transport-jsonl.js +4 -0
  70. package/tools/browse.d.ts +5 -0
  71. package/tools/browse.js +5 -0
  72. package/tools/defer.d.ts +13 -1
  73. package/tools/defer.js +13 -1
  74. package/tools/litectx-mcp.d.ts +6 -0
  75. package/tools/litectx-mcp.js +6 -0
  76. package/tools/mobile.d.ts +5 -0
  77. package/tools/mobile.js +5 -0
  78. package/tools/shell.d.ts +5 -0
  79. package/tools/shell.js +5 -0
  80. package/tools/spawn.d.ts +10 -0
  81. package/tools/spawn.js +10 -0
  82. package/types/index.d.ts +10 -0
package/src/complexity.js CHANGED
@@ -89,6 +89,11 @@ const MAX_ASSESS_LEN = 4000;
89
89
  * Assess the complexity of a goal/prompt from its text alone (no LLM).
90
90
  * @param {string} prompt - The goal to classify.
91
91
  * @returns {ComplexityResult}
92
+ * @when you want a fast, no-LLM read of how hard a goal is (simple/medium/complex/critical) to decide whether to invoke the Planner
93
+ * @fails never throws — pure text scoring; non-string/blank input scores lowest. Keyword lists are frozen and a critical-safety override always wins.
94
+ * @example
95
+ * const { level, needsPlanning } = assessComplexity(goal);
96
+ * if (needsPlanning) await planner.plan(goal);
92
97
  */
93
98
  function assessComplexity(prompt) {
94
99
  if (typeof prompt !== 'string' || !prompt.trim()) {
@@ -169,6 +174,10 @@ function assessComplexity(prompt) {
169
174
  * non-string / blank input is `false`.
170
175
  * @param {string} prompt - The goal to test.
171
176
  * @returns {boolean}
177
+ * @when you want the durable critical-safety floor alone (security/production/compliance/financial) to gate extra scrutiny, without the full scorer
178
+ * @fails never throws — deterministic override; non-string/blank input is false. This floor is non-overridable by design.
179
+ * @example
180
+ * if (isCritical(goal)) verdict = await evaluator.evaluate(goal, result, { contract });
172
181
  */
173
182
  function isCritical(prompt) {
174
183
  if (typeof prompt !== 'string' || !prompt.trim()) return false;
@@ -32,6 +32,10 @@ export function fromUnits(units: Array<Record<string, any>>): Array<Record<strin
32
32
  * error (incl. HaltError) is left to the Loop's own fail-open / HaltError handling — not swallowed here.
33
33
  * @param {(units: Array<Record<string, any>>, ctx: any) => (any | Promise<any>)} assembleUnits
34
34
  * @returns {(msgs: Array<Record<string, any>>, ctx: any) => Promise<Array<Record<string, any>>>}
35
+ * @when you want to adapt a litectx-style neutral-unit assemble(units, ctx) verb into the Loop's assemble(msgs, ctx) context-assembly seam
36
+ * @fails fail-open — any unexpected return shape sends the original msgs unchanged; a thrown error (incl. HaltError) is left to the Loop's own handling.
37
+ * @example
38
+ * const loop = new Loop({ provider, assemble: unitAssembler(litectx.assemble) });
35
39
  */
36
40
  export function unitAssembler(assembleUnits: (units: Array<Record<string, any>>, ctx: any) => (any | Promise<any>)): (msgs: Array<Record<string, any>>, ctx: any) => Promise<Array<Record<string, any>>>;
37
41
  /**
@@ -67,6 +71,10 @@ export function unitAssembler(assembleUnits: (units: Array<Record<string, any>>,
67
71
  * `({ key, content, unit }) => void|Promise` (REQUIRED; the harvest policy point). `policy` — litectx
68
72
  * TrimPolicy: `{ keepLastN }` or `{ maxTokens }` (maxTokens wins). Both verbs are runtime-checked.
69
73
  * @returns {((msgs: Array<Record<string, any>>, ctx?: any) => Promise<Array<Record<string, any>>>) & { flush: (msgs: Array<Record<string, any>>, ctx?: any) => Promise<void> }}
74
+ * @when you want to adapt litectx's trim(units, policy) verb into the Loop's destructive trim(msgs, ctx) seam — harvest-before-evict with an F2 residual .flush
75
+ * @fails throws if the trim/onHarvest verbs are missing (runtime-checked); the fold is fail-open and a HaltError propagates. `.flush` drains the residual harvest.
76
+ * @example
77
+ * const loop = new Loop({ provider, trim: unitTrimmer({ trim: litectx.trim, onHarvest, policy: { keepLastN: 20 } }) });
70
78
  */
71
79
  export function unitTrimmer(opts?: {
72
80
  trim?: Function;
@@ -93,6 +101,10 @@ export function unitTrimmer(opts?: {
93
101
  * distinct turns). Normal provider ids (`call_…`) round-trip unchanged through the escape.
94
102
  * @param {Record<string, any>} unit - a unit from {@link toUnits} (its `_msgs` backing is read).
95
103
  * @returns {string}
104
+ * @when you need the stable content-address for a transcript unit (the key harvest-before-evict writes under) — a collision-resistant 64-bit id
105
+ * @fails never throws; normal provider ids round-trip unchanged, and two near-independent hash streams avoid 32-bit birthday collisions.
106
+ * @example
107
+ * const key = harvestKey(unit); // stable id for this turn's harvest
96
108
  */
97
109
  export function harvestKey(unit: Record<string, any>): string;
98
110
  /** chars/4 token estimate over a list of messages (matches poc2 / the Loop's own heuristic). */
@@ -207,6 +207,10 @@ function fromUnits(units) {
207
207
  * error (incl. HaltError) is left to the Loop's own fail-open / HaltError handling — not swallowed here.
208
208
  * @param {(units: Array<Record<string, any>>, ctx: any) => (any | Promise<any>)} assembleUnits
209
209
  * @returns {(msgs: Array<Record<string, any>>, ctx: any) => Promise<Array<Record<string, any>>>}
210
+ * @when you want to adapt a litectx-style neutral-unit assemble(units, ctx) verb into the Loop's assemble(msgs, ctx) context-assembly seam
211
+ * @fails fail-open — any unexpected return shape sends the original msgs unchanged; a thrown error (incl. HaltError) is left to the Loop's own handling.
212
+ * @example
213
+ * const loop = new Loop({ provider, assemble: unitAssembler(litectx.assemble) });
210
214
  */
211
215
  function unitAssembler(assembleUnits) {
212
216
  if (typeof assembleUnits !== 'function') {
@@ -240,6 +244,10 @@ function unitAssembler(assembleUnits) {
240
244
  * distinct turns). Normal provider ids (`call_…`) round-trip unchanged through the escape.
241
245
  * @param {Record<string, any>} unit - a unit from {@link toUnits} (its `_msgs` backing is read).
242
246
  * @returns {string}
247
+ * @when you need the stable content-address for a transcript unit (the key harvest-before-evict writes under) — a collision-resistant 64-bit id
248
+ * @fails never throws; normal provider ids round-trip unchanged, and two near-independent hash streams avoid 32-bit birthday collisions.
249
+ * @example
250
+ * const key = harvestKey(unit); // stable id for this turn's harvest
243
251
  */
244
252
  function harvestKey(unit) {
245
253
  const back = (unit && unit._msgs) || [];
@@ -296,6 +304,10 @@ function harvestKey(unit) {
296
304
  * `({ key, content, unit }) => void|Promise` (REQUIRED; the harvest policy point). `policy` — litectx
297
305
  * TrimPolicy: `{ keepLastN }` or `{ maxTokens }` (maxTokens wins). Both verbs are runtime-checked.
298
306
  * @returns {((msgs: Array<Record<string, any>>, ctx?: any) => Promise<Array<Record<string, any>>>) & { flush: (msgs: Array<Record<string, any>>, ctx?: any) => Promise<void> }}
307
+ * @when you want to adapt litectx's trim(units, policy) verb into the Loop's destructive trim(msgs, ctx) seam — harvest-before-evict with an F2 residual .flush
308
+ * @fails throws if the trim/onHarvest verbs are missing (runtime-checked); the fold is fail-open and a HaltError propagates. `.flush` drains the residual harvest.
309
+ * @example
310
+ * const loop = new Loop({ provider, trim: unitTrimmer({ trim: litectx.trim, onHarvest, policy: { keepLastN: 20 } }) });
299
311
  */
300
312
  function unitTrimmer(opts) {
301
313
  const { trim, onHarvest, policy = {} } = opts || {};
@@ -114,7 +114,15 @@ export type EvaluateOptions = {
114
114
  * Built flagged-and-deletable per D11 — opt-in by import; calibrate the rubric/prompt from execution traces.
115
115
  */
116
116
  export class Evaluator {
117
- /** @param {EvaluatorOptions} [options] */
117
+ /**
118
+ * @param {EvaluatorOptions} [options]
119
+ * @when you need to judge an output against a goal or contract — deterministically (predicate), by LLM rubric, or with a tool-running critic that exercises the live artifact
120
+ * @fails never throws for a bad grade — returns a Verdict {status: satisfied|needs_revision|failed}; a provider HaltError propagates clean. Judge tokens forward via onLlmResult.
121
+ * @example
122
+ * const evaluator = new Evaluator({ provider });
123
+ * const verdict = await evaluator.evaluate(goal, result, { rubric });
124
+ * if (!verdict.pass) revise(verdict.critique);
125
+ */
118
126
  constructor(options?: EvaluatorOptions);
119
127
  provider: import("../types").Provider | null;
120
128
  prompt: string;
package/src/evaluator.js CHANGED
@@ -108,7 +108,15 @@ Output your FINAL answer as ONLY this JSON, no markdown, no prose:
108
108
  * Built flagged-and-deletable per D11 — opt-in by import; calibrate the rubric/prompt from execution traces.
109
109
  */
110
110
  class Evaluator {
111
- /** @param {EvaluatorOptions} [options] */
111
+ /**
112
+ * @param {EvaluatorOptions} [options]
113
+ * @when you need to judge an output against a goal or contract — deterministically (predicate), by LLM rubric, or with a tool-running critic that exercises the live artifact
114
+ * @fails never throws for a bad grade — returns a Verdict {status: satisfied|needs_revision|failed}; a provider HaltError propagates clean. Judge tokens forward via onLlmResult.
115
+ * @example
116
+ * const evaluator = new Evaluator({ provider });
117
+ * const verdict = await evaluator.evaluate(goal, result, { rubric });
118
+ * if (!verdict.pass) revise(verdict.critique);
119
+ */
112
120
  constructor(options = /** @type {EvaluatorOptions} */ ({})) {
113
121
  this.provider = options.provider || null;
114
122
  this.prompt = options.prompt || GRADER_PROMPT;
@@ -129,6 +129,11 @@ export function gradeRun(runs: Array<{
129
129
  * a separate admission gate — a leak in any style blocks admission even at a passing clear-case floor.
130
130
  * @param {(payload:object)=>any} [opts.onLlmResult] - Budget hook forwarded to each judge call.
131
131
  * @returns {Promise<ReturnType<typeof gradeRun> & { reps:number, floor:number, totalCostUsd:number|null, unpricedCalls:number, injectionBattery: { styles: Array<{label:string, usable:number, broke:number, resisted:boolean}>, allResisted:boolean, leaks:number } }>}
132
+ * @when you are admitting an LLM tier to the judge role and need to grade it against the frozen clear-case battery and resist every injection style before trusting it
133
+ * @fails never fabricates a pass — a leak in any injection style blocks admission even above the clear-case floor, and the negative control must fail the set. HaltError propagates clean.
134
+ * @example
135
+ * const report = await calibrate({ provider, reps: 5, floor: 7 });
136
+ * if (!report.injectionBattery.allResisted) reject('injection leak');
132
137
  */
133
138
  export function calibrate(opts?: {
134
139
  provider: import("../types").Provider;
@@ -125,6 +125,11 @@ function gradeRun(runs, floor) {
125
125
  * a separate admission gate — a leak in any style blocks admission even at a passing clear-case floor.
126
126
  * @param {(payload:object)=>any} [opts.onLlmResult] - Budget hook forwarded to each judge call.
127
127
  * @returns {Promise<ReturnType<typeof gradeRun> & { reps:number, floor:number, totalCostUsd:number|null, unpricedCalls:number, injectionBattery: { styles: Array<{label:string, usable:number, broke:number, resisted:boolean}>, allResisted:boolean, leaks:number } }>}
128
+ * @when you are admitting an LLM tier to the judge role and need to grade it against the frozen clear-case battery and resist every injection style before trusting it
129
+ * @fails never fabricates a pass — a leak in any injection style blocks admission even above the clear-case floor, and the negative control must fail the set. HaltError propagates clean.
130
+ * @example
131
+ * const report = await calibrate({ provider, reps: 5, floor: 7 });
132
+ * if (!report.injectionBattery.allResisted) reject('injection leak');
128
133
  */
129
134
  async function calibrate(opts = /** @type {any} */ ({})) {
130
135
  const provider = opts.provider;
package/src/judge.d.ts CHANGED
@@ -157,6 +157,11 @@ export type JudgeVerdict = {
157
157
  * @throws {ValidationError} on bad inputs (missing request/artifact/provider) — stamped `context.lib='bare-agent'`
158
158
  * at the throw site (contract 4: typed attribution, never sniffed from prose).
159
159
  * @throws {HaltError} propagated clean from the provider (a governance halt is not a judge failure).
160
+ * @when you need a decisive honored/broke verdict on whether one egress artifact honored the verbatim request — a return-time integrity check
161
+ * @fails throws ValidationError on bad inputs; HaltError propagates clean. Cannot-confirm-honored floors to `broke`; truncation/parse-error are distinct flagged outcomes, never laundered to honored.
162
+ * @example
163
+ * const { verdict, where } = await judge({ request, artifact, provider });
164
+ * if (verdict === 'broke') flag(where);
160
165
  */
161
166
  export function judge(options?: JudgeOptions): Promise<JudgeVerdict>;
162
167
  /**
package/src/judge.js CHANGED
@@ -145,6 +145,11 @@ function normalizeWhere(where) {
145
145
  * @throws {ValidationError} on bad inputs (missing request/artifact/provider) — stamped `context.lib='bare-agent'`
146
146
  * at the throw site (contract 4: typed attribution, never sniffed from prose).
147
147
  * @throws {HaltError} propagated clean from the provider (a governance halt is not a judge failure).
148
+ * @when you need a decisive honored/broke verdict on whether one egress artifact honored the verbatim request — a return-time integrity check
149
+ * @fails throws ValidationError on bad inputs; HaltError propagates clean. Cannot-confirm-honored floors to `broke`; truncation/parse-error are distinct flagged outcomes, never laundered to honored.
150
+ * @example
151
+ * const { verdict, where } = await judge({ request, artifact, provider });
152
+ * if (verdict === 'broke') flag(where);
148
153
  */
149
154
  async function judge(options = /** @type {JudgeOptions} */ ({})) {
150
155
  const { request, artifact, provider } = options;
package/src/loop.d.ts CHANGED
@@ -105,6 +105,12 @@ export class Loop {
105
105
  * tool outcomes to `gate.record` (via wireGate) and never kill the loop on error.
106
106
  * @param {LoopOptions} options
107
107
  * @throws {Error} `[Loop] requires a provider` — when options.provider is missing.
108
+ * @when you are running a model think/act/observe cycle and need round accounting, tool dispatch, spin guards, and a governance chokepoint
109
+ * @fails returns the last assistant text with an `error` tag on any bound (halt/deny-streak/truncation/provider error); throws only if constructed without a provider. A policy HaltError exits clean.
110
+ * @example
111
+ * const loop = new Loop({ provider, policy });
112
+ * const { text, error, metrics } = await loop.run(messages, tools);
113
+ * if (error) handle(error);
108
114
  */
109
115
  constructor(options?: LoopOptions);
110
116
  provider: import("../types").Provider;
@@ -161,7 +167,7 @@ export class Loop {
161
167
  * thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
162
168
  * unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
163
169
  * @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
164
- * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
170
+ * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: {name: string|undefined, error: string}, temperatureDropped?: boolean}>}
165
171
  * On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
166
172
  * thrown HaltError carried no `rule`), and `msgs` is sanitized so any
167
173
  * dangling assistant `tool_calls` from the halted round are paired with
@@ -199,6 +205,10 @@ export class Loop {
199
205
  model: string | null;
200
206
  msgs: Message[];
201
207
  metrics: RunMetrics;
208
+ malformedToolCall?: {
209
+ name: string | undefined;
210
+ error: string;
211
+ };
202
212
  temperatureDropped?: boolean;
203
213
  }>;
204
214
  _warnedTruncated: boolean | undefined;
@@ -228,7 +238,7 @@ export class Loop {
228
238
  * @param {string} text - User message.
229
239
  * @param {ToolDef[]} [tools=[]] - Tool definitions.
230
240
  * @param {Record<string, any>} [options={}] - Per-run overrides.
231
- * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
241
+ * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: {name: string|undefined, error: string}, temperatureDropped?: boolean}>}
232
242
  */
233
243
  chat(text: string, tools?: ToolDef[], options?: Record<string, any>): Promise<{
234
244
  text: string;
@@ -240,6 +250,10 @@ export class Loop {
240
250
  model: string | null;
241
251
  msgs: Message[];
242
252
  metrics: RunMetrics;
253
+ malformedToolCall?: {
254
+ name: string | undefined;
255
+ error: string;
256
+ };
243
257
  temperatureDropped?: boolean;
244
258
  }>;
245
259
  /**
package/src/loop.js CHANGED
@@ -273,6 +273,12 @@ class Loop {
273
273
  * tool outcomes to `gate.record` (via wireGate) and never kill the loop on error.
274
274
  * @param {LoopOptions} options
275
275
  * @throws {Error} `[Loop] requires a provider` — when options.provider is missing.
276
+ * @when you are running a model think/act/observe cycle and need round accounting, tool dispatch, spin guards, and a governance chokepoint
277
+ * @fails returns the last assistant text with an `error` tag on any bound (halt/deny-streak/truncation/provider error); throws only if constructed without a provider. A policy HaltError exits clean.
278
+ * @example
279
+ * const loop = new Loop({ provider, policy });
280
+ * const { text, error, metrics } = await loop.run(messages, tools);
281
+ * if (error) handle(error);
276
282
  */
277
283
  constructor(options = /** @type {LoopOptions} */ ({})) {
278
284
  if (!options.provider) throw new Error('[Loop] requires a provider');
@@ -449,7 +455,7 @@ class Loop {
449
455
  * thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
450
456
  * unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
451
457
  * @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
452
- * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
458
+ * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: {name: string|undefined, error: string}, temperatureDropped?: boolean}>}
453
459
  * On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
454
460
  * thrown HaltError carried no `rule`), and `msgs` is sanitized so any
455
461
  * dangling assistant `tool_calls` from the halted round are paired with
@@ -547,6 +553,14 @@ class Loop {
547
553
  // RESPONSE, not the provider object — a wrapped/fallback provider can lose `.model`). Surfaced on
548
554
  // every return so a caller reads which model produced the result without the onLlmResult side channel.
549
555
  let lastModel = null;
556
+ // BA-27: the most recent round's malformed-tool-call marker, or null. A model can emit a tool call
557
+ // whose arguments are syntactically-broken JSON; the provider returns NO usable tool calls plus this
558
+ // marker (rather than throwing and losing the billed round). Surfaced on the run's return like
559
+ // lastStopReason, so a Loop caller (e.g. the bareloop adopter — which reads run(), not generate())
560
+ // can tell "the model sent a broken call" apart from "the model sent no call at all" — both present
561
+ // as `toolCalls: []`. Reset each completed round; a malformed round always terminates the run (no
562
+ // usable call to continue with), so it can only be non-null on the round that returns.
563
+ let lastMalformedToolCall = null;
550
564
  // BA-10: sticky across rounds — true if ANY round's `temperature` was dropped by the model (400,
551
565
  // unsupported/deprecated) and retried without it. Surfaced on the result so an upstream receipt
552
566
  // (recurse's refineLeaf) can report the EFFECTIVE temperature rather than the ignored request.
@@ -583,7 +597,7 @@ class Loop {
583
597
  sealDanglingToolCalls(msgs, `[halted:${stuckTag}]`);
584
598
  this._reportError('stuck', new Error(`tool "${tc.name}" failed ${identicalErrors} times with identical arguments`), { rule: stuckTag, attempts: identicalErrors });
585
599
  this._safeEmit({ type: 'loop:done', data: { text: lastText, stuck: true, rule: stuckTag, cost: totalCost } });
586
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: stuckTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
600
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: stuckTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
587
601
  }
588
602
  return null;
589
603
  };
@@ -830,7 +844,7 @@ class Loop {
830
844
  this._reportError('provider', err, { round });
831
845
  if (this.throwOnError) throw err;
832
846
  // BA-5: a mid-run provider failure must not erase the work of the rounds that succeeded.
833
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: err.message, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
847
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: err.message, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
834
848
  }
835
849
 
836
850
  lastUsage = result.usage || lastUsage;
@@ -843,6 +857,9 @@ class Loop {
843
857
  // path reads lastStopReason). Non-string / absent ⇒ null (the provider's pre-BA-6 degrade).
844
858
  lastStopReason = typeof result.stopReason === 'string' ? result.stopReason : null;
845
859
  if (typeof result.model === 'string' && result.model) lastModel = result.model;
860
+ // BA-27: capture (and reset) this round's malformed-tool-call marker for surfacing on the return.
861
+ lastMalformedToolCall = (result.malformedToolCall && typeof result.malformedToolCall === 'object')
862
+ ? result.malformedToolCall : null;
846
863
  if (result.temperatureDropped) temperatureDropped = true;
847
864
  // Publish the latest measured usage to ctx (non-enumerable, fail-open) so a transcript-bound seam —
848
865
  // e.g. F2 stash auto-compaction — can read EXACT provider-counted `inputTokens` to gauge context
@@ -927,7 +944,7 @@ class Loop {
927
944
  sealDanglingToolCalls(msgs, `[halted:${session.error}]`);
928
945
  this._reportError('session', new Error(`provider session terminated: ${session.error}`), { rule: session.error, sessionTurns: session.turns ?? null });
929
946
  this._safeEmit({ type: 'loop:done', data: { text: lastText, rule: session.error, sessionTurns: session.turns ?? null, cost: totalCost } });
930
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: session.error, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
947
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: session.error, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
931
948
  }
932
949
 
933
950
  // BA-13: classify this round's terminal signal against the neutral stop-reason vocabulary. BA-6
@@ -992,7 +1009,7 @@ class Loop {
992
1009
  msgs.push({ role: 'assistant', content: result.text });
993
1010
  }
994
1011
  this._safeEmit({ type: 'loop:done', data: { text: lastText, ...(terminal === 'truncated' && { truncated: true }), terminal, stopReason: lastStopReason, droppedToolCalls: dropped, cost: totalCost } });
995
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: errorTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
1012
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: errorTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
996
1013
  }
997
1014
 
998
1015
  // No tool calls — LLM gave a final text response
@@ -1012,7 +1029,7 @@ class Loop {
1012
1029
  try { await flush(msgs, ctx); }
1013
1030
  catch (err) { if (err instanceof HaltError) throw err; this._reportError('trim-flush', err, { round }); }
1014
1031
  }
1015
- return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
1032
+ return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
1016
1033
  }
1017
1034
 
1018
1035
  // Execute tool calls
@@ -1121,7 +1138,7 @@ class Loop {
1121
1138
  sealDanglingToolCalls(msgs, `[halted:${denyTag}]`);
1122
1139
  this._reportError('denied', new Error(`policy denied ${consecutiveDenials} consecutive tool calls (${tc.name})`), { rule: denyTag, denials: consecutiveDenials });
1123
1140
  this._safeEmit({ type: 'loop:done', data: { text: lastText, denied: true, rule: denyTag, cost: totalCost } });
1124
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: denyTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
1141
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: denyTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
1125
1142
  }
1126
1143
  continue;
1127
1144
  }
@@ -1195,7 +1212,7 @@ class Loop {
1195
1212
  // BA-5: the rule tag survives on `error`; so does the work. A halt is how a bounded attempt is
1196
1213
  // SUPPOSED to end — the caller reads `error` to know it was bounded and `text` to learn from it.
1197
1214
  this._safeEmit({ type: 'loop:done', data: { text: lastText, halted: true, rule, cost: totalCost } });
1198
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
1215
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
1199
1216
  }
1200
1217
  throw err;
1201
1218
  }
@@ -1225,20 +1242,20 @@ class Loop {
1225
1242
  const rule = err.rule || 'unknown';
1226
1243
  this._reportError('halt', err, { rule, reason: err.decision?.reason ?? null });
1227
1244
  this._safeEmit({ type: 'loop:done', data: { text: lastText, halted: true, rule, cost: totalCost } });
1228
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
1245
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
1229
1246
  }
1230
1247
  this._reportError('trim-flush', err, { phase: 'stop' });
1231
1248
  }
1232
1249
  }
1233
1250
  this._safeEmit({ type: 'loop:done', data: { text: lastText, stopped: true, cost: totalCost } });
1234
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
1251
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
1235
1252
  }
1236
1253
 
1237
1254
  // Hard safety limit — should never fire under normal usage; bareguard's
1238
1255
  // limits.maxTurns (or the LLM's natural completion) ends the loop first.
1239
1256
  const warning = `[Loop] hit internal safety limit of ${HARD_ROUND_LIMIT} rounds. Wire bareguard for proper governance — see bare-agent/bareguard.`;
1240
1257
  this._safeEmit({ type: 'loop:done', data: { text: lastText, warning, cost: totalCost } });
1241
- return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: warning, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
1258
+ return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: warning, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }) };
1242
1259
  }
1243
1260
 
1244
1261
  /**
@@ -1311,7 +1328,7 @@ class Loop {
1311
1328
  * @param {string} text - User message.
1312
1329
  * @param {ToolDef[]} [tools=[]] - Tool definitions.
1313
1330
  * @param {Record<string, any>} [options={}] - Per-run overrides.
1314
- * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
1331
+ * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: {name: string|undefined, error: string}, temperatureDropped?: boolean}>}
1315
1332
  */
1316
1333
  async chat(text, tools = [], options = {}) {
1317
1334
  this._history.push({ role: 'user', content: text });
@@ -92,6 +92,11 @@ export type RpcClient = {
92
92
  * of this hook also opts default discovery into the project-cwd `./.mcp.json`,
93
93
  * since each command is then vetted regardless of source.
94
94
  * @returns {Promise<{tools: ToolDef[], metaTools?: ToolDef[], servers: string[], systemContext: string, denied: DeniedTool[], errors?: Array<{server: string, error: string}>, close: Function}>}
95
+ * @when you want to auto-discover MCP servers and expose them as bareagent tools in one call — with a trust hook gating command execution
96
+ * @fails a trust hook returning false skips a server (its command never runs) and a throw is fail-closed (deny); returns tools plus a close() to shut servers down.
97
+ * @example
98
+ * const { tools, close } = await createMCPBridge();
99
+ * const loop = new Loop({ provider, tools });
95
100
  */
96
101
  export function createMCPBridge(opts?: {
97
102
  bridgePath?: string | undefined;
@@ -120,6 +125,10 @@ export function createMCPBridge(opts?: {
120
125
  * @param {{ includeProjectConfig?: boolean }} [opts] - When no explicit `configPaths` are given,
121
126
  * set `includeProjectConfig: true` to also scan `./.mcp.json`. Default false — see PROJECT_CONFIG_PATH.
122
127
  * @returns {Map<string, ServerDef>}
128
+ * @when you want to find configured MCP servers from the trusted $HOME/IDE config paths (or explicit paths) — discovery without invoking them
129
+ * @fails honors explicit configPaths verbatim and scans trusted defaults otherwise; returns a Map and never executes a server command.
130
+ * @example
131
+ * const servers = discoverServers();
123
132
  */
124
133
  export function discoverServers(configPaths?: string[], { includeProjectConfig }?: {
125
134
  includeProjectConfig?: boolean;
@@ -142,5 +151,9 @@ export function discoverServers(configPaths?: string[], { includeProjectConfig }
142
151
  * @param {ToolDef[]} tools - The bulk-loaded, name-prefixed tools array.
143
152
  * @param {string} [discoveredAt] - ISO timestamp from .mcp-bridge.json.
144
153
  * @returns {ToolDef[]} [mcp_discover, mcp_invoke]
154
+ * @when you want the two bulk MCP meta-tools (mcp_discover, mcp_invoke) instead of exposing every discovered tool individually — one gate-check per invocation
155
+ * @fails returns [mcp_discover, mcp_invoke]; a tool name does not travel as action.type (a deliberate v0.9 trade for one gate-check per call).
156
+ * @example
157
+ * const metaTools = buildMetaTools(tools);
145
158
  */
146
159
  export function buildMetaTools(tools: ToolDef[], discoveredAt?: string): ToolDef[];
package/src/mcp-bridge.js CHANGED
@@ -80,6 +80,10 @@ const TRUSTED_CONFIG_PATHS = [
80
80
  * @param {{ includeProjectConfig?: boolean }} [opts] - When no explicit `configPaths` are given,
81
81
  * set `includeProjectConfig: true` to also scan `./.mcp.json`. Default false — see PROJECT_CONFIG_PATH.
82
82
  * @returns {Map<string, ServerDef>}
83
+ * @when you want to find configured MCP servers from the trusted $HOME/IDE config paths (or explicit paths) — discovery without invoking them
84
+ * @fails honors explicit configPaths verbatim and scans trusted defaults otherwise; returns a Map and never executes a server command.
85
+ * @example
86
+ * const servers = discoverServers();
83
87
  */
84
88
  function discoverServers(configPaths, { includeProjectConfig = false } = {}) {
85
89
  let paths;
@@ -523,6 +527,10 @@ function buildSystemContext(servers, tools, denied) {
523
527
  * @param {ToolDef[]} tools - The bulk-loaded, name-prefixed tools array.
524
528
  * @param {string} [discoveredAt] - ISO timestamp from .mcp-bridge.json.
525
529
  * @returns {ToolDef[]} [mcp_discover, mcp_invoke]
530
+ * @when you want the two bulk MCP meta-tools (mcp_discover, mcp_invoke) instead of exposing every discovered tool individually — one gate-check per invocation
531
+ * @fails returns [mcp_discover, mcp_invoke]; a tool name does not travel as action.type (a deliberate v0.9 trade for one gate-check per call).
532
+ * @example
533
+ * const metaTools = buildMetaTools(tools);
526
534
  */
527
535
  function buildMetaTools(tools, discoveredAt) {
528
536
  // Catalog descriptors: same info the LLM would see for bulk-loaded tools,
@@ -637,6 +645,11 @@ function buildMetaTools(tools, discoveredAt) {
637
645
  * of this hook also opts default discovery into the project-cwd `./.mcp.json`,
638
646
  * since each command is then vetted regardless of source.
639
647
  * @returns {Promise<{tools: ToolDef[], metaTools?: ToolDef[], servers: string[], systemContext: string, denied: DeniedTool[], errors?: Array<{server: string, error: string}>, close: Function}>}
648
+ * @when you want to auto-discover MCP servers and expose them as bareagent tools in one call — with a trust hook gating command execution
649
+ * @fails a trust hook returning false skips a server (its command never runs) and a throw is fail-closed (deny); returns tools plus a close() to shut servers down.
650
+ * @example
651
+ * const { tools, close } = await createMCPBridge();
652
+ * const loop = new Loop({ provider, tools });
640
653
  */
641
654
  async function createMCPBridge(opts = {}) {
642
655
  if ('policy' in opts) {
package/src/memory.d.ts CHANGED
@@ -19,6 +19,11 @@ export class Memory {
19
19
  /**
20
20
  * @param {{ store?: Store }} [options] - Store backend (must implement store/search/get/delete).
21
21
  * @throws {Error} `[Memory] requires options.store` — when options.store is missing.
22
+ * @when you want thin store-backed memory (store/search/get/delete) an agent can write to and recall from, over any swappable backend
23
+ * @fails throws if constructed without a store; recall/store metering is opt-in via ctx. All persistence delegates to the backend.
24
+ * @example
25
+ * const memory = new Memory({ store: new JsonFile({ path: './mem.json' }) });
26
+ * await memory.store({ text: 'a durable fact' });
22
27
  */
23
28
  constructor(options?: {
24
29
  store?: Store;
package/src/memory.js CHANGED
@@ -21,6 +21,11 @@ class Memory {
21
21
  /**
22
22
  * @param {{ store?: Store }} [options] - Store backend (must implement store/search/get/delete).
23
23
  * @throws {Error} `[Memory] requires options.store` — when options.store is missing.
24
+ * @when you want thin store-backed memory (store/search/get/delete) an agent can write to and recall from, over any swappable backend
25
+ * @fails throws if constructed without a store; recall/store metering is opt-in via ctx. All persistence delegates to the backend.
26
+ * @example
27
+ * const memory = new Memory({ store: new JsonFile({ path: './mem.json' }) });
28
+ * await memory.store({ text: 'a durable fact' });
24
29
  */
25
30
  constructor(options = {}) {
26
31
  if (!options.store) throw new Error('[Memory] requires options.store');
package/src/planner.d.ts CHANGED
@@ -46,6 +46,12 @@ export class Planner {
46
46
  /**
47
47
  * @param {PlannerOptions} options
48
48
  * @throws {Error} `[Planner] requires a provider` — when options.provider is missing.
49
+ * @when you need to turn a goal into an ordered step DAG for an LLM to execute — optionally forcing exactly N independent steps for fan-out
50
+ * @fails throws if constructed without a provider; a plan call's HaltError (governance cap) propagates clean. Plan-call usage forwards via onLlmResult.
51
+ * @example
52
+ * const planner = new Planner({ provider });
53
+ * const steps = await planner.plan('ship the release', { count: 4 });
54
+ * await runPlan(steps, ctx);
49
55
  */
50
56
  constructor(options?: PlannerOptions);
51
57
  provider: import("../types").Provider;
package/src/planner.js CHANGED
@@ -39,6 +39,12 @@ class Planner {
39
39
  /**
40
40
  * @param {PlannerOptions} options
41
41
  * @throws {Error} `[Planner] requires a provider` — when options.provider is missing.
42
+ * @when you need to turn a goal into an ordered step DAG for an LLM to execute — optionally forcing exactly N independent steps for fan-out
43
+ * @fails throws if constructed without a provider; a plan call's HaltError (governance cap) propagates clean. Plan-call usage forwards via onLlmResult.
44
+ * @example
45
+ * const planner = new Planner({ provider });
46
+ * const steps = await planner.plan('ship the release', { count: 4 });
47
+ * await runPlan(steps, ctx);
42
48
  */
43
49
  constructor(options = /** @type {PlannerOptions} */ ({})) {
44
50
  if (!options.provider) throw new Error('[Planner] requires a provider');
@@ -62,6 +62,10 @@ export class AnthropicProvider {
62
62
  /**
63
63
  * @param {AnthropicOptions} [options]
64
64
  * @throws {Error} `[AnthropicProvider] requires apiKey` — when apiKey is missing.
65
+ * @when you want Claude models as the Loop's provider — native Messages API with opt-in prompt caching and thinking-block passthrough
66
+ * @fails throws on a missing apiKey; normalizes stopReason and usage (usage:null when the API omits it); a socket idle/deadline/transport cut rejects with a retryable Timeout/ProviderError.
67
+ * @example
68
+ * const provider = new AnthropicProvider({ apiKey, model: 'claude-sonnet-5' });
65
69
  */
66
70
  constructor(options?: AnthropicOptions);
67
71
  apiKey: string;
@@ -41,6 +41,10 @@ class AnthropicProvider {
41
41
  /**
42
42
  * @param {AnthropicOptions} [options]
43
43
  * @throws {Error} `[AnthropicProvider] requires apiKey` — when apiKey is missing.
44
+ * @when you want Claude models as the Loop's provider — native Messages API with opt-in prompt caching and thinking-block passthrough
45
+ * @fails throws on a missing apiKey; normalizes stopReason and usage (usage:null when the API omits it); a socket idle/deadline/transport cut rejects with a retryable Timeout/ProviderError.
46
+ * @example
47
+ * const provider = new AnthropicProvider({ apiKey, model: 'claude-sonnet-5' });
44
48
  */
45
49
  constructor(options = {}) {
46
50
  if (!options.apiKey) throw new Error('[AnthropicProvider] requires apiKey');
@@ -123,6 +123,10 @@ export class CLIPipeProvider {
123
123
  * Provider that pipes prompts to a CLI command via stdin and reads stdout.
124
124
  * @param {CLIPipeOptions} [options]
125
125
  * @throws {Error} `[CLIPipeProvider] requires command` — when options.command is missing.
126
+ * @when you want to drive a CLI (e.g. the claude CLI) as the Loop's provider — native MCP tools or a JSON emulation envelope
127
+ * @fails throws on a missing command; in native mode it owns its own cycle (ownsCycle), so per-round Loop seams are refused at construction rather than left silently dead.
128
+ * @example
129
+ * const provider = new CLIPipeProvider({ command: 'claude' });
126
130
  */
127
131
  constructor(options?: CLIPipeOptions);
128
132
  command: string;
@@ -112,6 +112,10 @@ class CLIPipeProvider {
112
112
  * Provider that pipes prompts to a CLI command via stdin and reads stdout.
113
113
  * @param {CLIPipeOptions} [options]
114
114
  * @throws {Error} `[CLIPipeProvider] requires command` — when options.command is missing.
115
+ * @when you want to drive a CLI (e.g. the claude CLI) as the Loop's provider — native MCP tools or a JSON emulation envelope
116
+ * @fails throws on a missing command; in native mode it owns its own cycle (ownsCycle), so per-round Loop seams are refused at construction rather than left silently dead.
117
+ * @example
118
+ * const provider = new CLIPipeProvider({ command: 'claude' });
115
119
  */
116
120
  constructor(options = {}) {
117
121
  if (!options.command) throw new Error('[CLIPipeProvider] requires command');
@@ -27,6 +27,10 @@ export class FallbackProvider {
27
27
  * @param {Provider[]} providers - Ordered list of providers with generate().
28
28
  * @param {FallbackOptions} [options={}]
29
29
  * @throws {Error} `[FallbackProvider] requires at least one provider` — when providers is empty.
30
+ * @when you want to try several providers in order, failing over to the next when one errors — resilience across tiers or vendors
31
+ * @fails throws if given no providers; returns the first provider's success, else propagates the last provider's error.
32
+ * @example
33
+ * const provider = new FallbackProvider([primary, backup]);
30
34
  */
31
35
  constructor(providers: Provider[], options?: FallbackOptions);
32
36
  providers: import("../types").Provider[];
@@ -17,6 +17,10 @@ class FallbackProvider {
17
17
  * @param {Provider[]} providers - Ordered list of providers with generate().
18
18
  * @param {FallbackOptions} [options={}]
19
19
  * @throws {Error} `[FallbackProvider] requires at least one provider` — when providers is empty.
20
+ * @when you want to try several providers in order, failing over to the next when one errors — resilience across tiers or vendors
21
+ * @fails throws if given no providers; returns the first provider's success, else propagates the last provider's error.
22
+ * @example
23
+ * const provider = new FallbackProvider([primary, backup]);
20
24
  */
21
25
  constructor(providers, options = {}) {
22
26
  if (!Array.isArray(providers) || providers.length === 0) {
@@ -45,7 +45,13 @@ export type GeminiOptions = {
45
45
  * cache-read tier populates with no opt-in.
46
46
  */
47
47
  export class GeminiProvider {
48
- /** @param {GeminiOptions} [options] */
48
+ /**
49
+ * @param {GeminiOptions} [options]
50
+ * @when you want Gemini models as the Loop's provider — native generateContent with implicit prompt caching
51
+ * @fails throws on a missing apiKey; normalizes stopReason (promoting a complete tool call) and usage; a socket idle/deadline/transport cut rejects with a retryable error.
52
+ * @example
53
+ * const provider = new GeminiProvider({ apiKey, model: 'gemini-2.5-pro' });
54
+ */
49
55
  constructor(options?: GeminiOptions);
50
56
  apiKey: string | undefined;
51
57
  model: string;
@@ -40,7 +40,13 @@ function isLoopbackHost(hostname) {
40
40
  * cache-read tier populates with no opt-in.
41
41
  */
42
42
  class GeminiProvider {
43
- /** @param {GeminiOptions} [options] */
43
+ /**
44
+ * @param {GeminiOptions} [options]
45
+ * @when you want Gemini models as the Loop's provider — native generateContent with implicit prompt caching
46
+ * @fails throws on a missing apiKey; normalizes stopReason (promoting a complete tool call) and usage; a socket idle/deadline/transport cut rejects with a retryable error.
47
+ * @example
48
+ * const provider = new GeminiProvider({ apiKey, model: 'gemini-2.5-pro' });
49
+ */
44
50
  constructor(options = {}) {
45
51
  this.apiKey = options.apiKey?.trim();
46
52
  this.model = options.model || 'gemini-2.5-flash';
@@ -28,6 +28,10 @@ export type OllamaOptions = {
28
28
  export class OllamaProvider {
29
29
  /**
30
30
  * @param {OllamaOptions} [options]
31
+ * @when you want local models via Ollama as the Loop's provider — no API key, self-hosted
32
+ * @fails normalizes stopReason (promoting a complete tool call) and usage; a malformed tool-call JSON returns no usable calls with usage metered; a socket idle/deadline cut rejects with a retryable error.
33
+ * @example
34
+ * const provider = new OllamaProvider({ model: 'llama3' });
31
35
  */
32
36
  constructor(options?: OllamaOptions);
33
37
  model: string;