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
@@ -6,6 +6,7 @@ const { requestWithTemperatureFallback } = require('./provider-temperature');
6
6
  const { normalizeStopReason } = require('./provider-stop-reason');
7
7
  const { resolveTimeoutMs, applyRequestBounds, guardResponseSettles } = require('./provider-http');
8
8
  const { hasUsageSignal } = require('./provider-usage');
9
+ const { parseToolCalls } = require('./provider-toolcalls');
9
10
 
10
11
  // BA-24: raw Ollama usage fields. Any present (even 0) ⇒ a usage signal; none ⇒ null (unpriceable).
11
12
  const OLLAMA_USAGE_KEYS = ['prompt_eval_count', 'eval_count'];
@@ -26,6 +27,10 @@ const OLLAMA_USAGE_KEYS = ['prompt_eval_count', 'eval_count'];
26
27
  class OllamaProvider {
27
28
  /**
28
29
  * @param {OllamaOptions} [options]
30
+ * @when you want local models via Ollama as the Loop's provider — no API key, self-hosted
31
+ * @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.
32
+ * @example
33
+ * const provider = new OllamaProvider({ model: 'llama3' });
29
34
  */
30
35
  constructor(options = {}) {
31
36
  this.model = options.model || 'llama3.2';
@@ -85,8 +90,11 @@ class OllamaProvider {
85
90
  });
86
91
  const msg = data.message || {};
87
92
 
88
- /** @type {import('../types').ToolCall[]} */
89
- const toolCalls = (msg.tool_calls || []).map((/** @type {any} */ tc) => ({
93
+ // BA-27: Ollama returns `function.arguments` as an OBJECT for well-formed calls, but some builds
94
+ // pass it through as a model-generated STRING a malformed one must not throw here (the round
95
+ // already billed; a throw loses usage + hangs metering). Mirror the OpenAI path: no usable calls
96
+ // + a marker, never repair. The object case is untouched (JSON.parse only runs on a string).
97
+ const { toolCalls, malformedToolCall } = parseToolCalls(msg.tool_calls, (/** @type {any} */ tc) => ({
90
98
  id: tc.id || `call_${Date.now()}`,
91
99
  name: tc.function.name,
92
100
  arguments: typeof tc.function.arguments === 'string'
@@ -97,6 +105,7 @@ class OllamaProvider {
97
105
  return {
98
106
  text: msg.content || '',
99
107
  toolCalls,
108
+ ...(malformedToolCall && { malformedToolCall }),
100
109
  model: data.model || this.model,
101
110
  // BA-6: `length` ⇒ cut off at num_predict. VERIFIED LIVE on qwen2.5:0.5b
102
111
  // (`poc/ba6-stop-reason-gemini-ollama.mjs`): stop→end_turn, length→max_tokens. Lifecycle values
@@ -68,6 +68,10 @@ export type OpenAIOptions = {
68
68
  export class OpenAIProvider {
69
69
  /**
70
70
  * @param {OpenAIOptions} [options]
71
+ * @when you want OpenAI or any OpenAI-compatible endpoint as the Loop's provider — tool-calling, toolChoice, and a custom baseUrl
72
+ * @fails throws on a missing apiKey; a malformed tool-call JSON returns no usable calls with usage still metered (BA-27); a socket idle/deadline/transport cut rejects with a retryable error.
73
+ * @example
74
+ * const provider = new OpenAIProvider({ apiKey, model: 'gpt-5' });
71
75
  */
72
76
  constructor(options?: OpenAIOptions);
73
77
  apiKey: string | undefined;
@@ -7,6 +7,7 @@ const { requestWithTemperatureFallback } = require('./provider-temperature');
7
7
  const { normalizeStopReason } = require('./provider-stop-reason');
8
8
  const { resolveTimeoutMs, applyRequestBounds, guardResponseSettles } = require('./provider-http');
9
9
  const { hasUsageSignal } = require('./provider-usage');
10
+ const { parseToolCalls } = require('./provider-toolcalls');
10
11
 
11
12
  // BA-24: raw OpenAI usage fields. Any present (even 0) ⇒ a usage signal; none ⇒ null (unpriceable).
12
13
  const OPENAI_USAGE_KEYS = ['prompt_tokens', 'completion_tokens', 'prompt_tokens_details'];
@@ -30,7 +31,13 @@ function toOpenAIToolChoice(choice) {
30
31
  if (typeof choice === 'object' && typeof choice.name === 'string' && choice.name) {
31
32
  return { type: 'function', function: { name: choice.name } };
32
33
  }
33
- throw new ProviderError(`[OpenAIProvider] invalid toolChoice: expected 'auto', 'required', or { name }, got ${JSON.stringify(choice)}`);
34
+ let describedChoice;
35
+ try {
36
+ describedChoice = JSON.stringify(choice);
37
+ } catch {
38
+ describedChoice = '<unserializable>';
39
+ }
40
+ throw new ProviderError(`[OpenAIProvider] invalid toolChoice: expected 'auto', 'required', or { name }, got ${describedChoice}`);
34
41
  }
35
42
 
36
43
  /** @param {string} hostname @returns {boolean} */
@@ -69,6 +76,10 @@ function isLoopbackHost(hostname) {
69
76
  class OpenAIProvider {
70
77
  /**
71
78
  * @param {OpenAIOptions} [options]
79
+ * @when you want OpenAI or any OpenAI-compatible endpoint as the Loop's provider — tool-calling, toolChoice, and a custom baseUrl
80
+ * @fails throws on a missing apiKey; a malformed tool-call JSON returns no usable calls with usage still metered (BA-27); a socket idle/deadline/transport cut rejects with a retryable error.
81
+ * @example
82
+ * const provider = new OpenAIProvider({ apiKey, model: 'gpt-5' });
72
83
  */
73
84
  constructor(options = {}) {
74
85
  this.apiKey = options.apiKey?.trim();
@@ -125,11 +136,27 @@ class OpenAIProvider {
125
136
  stripTemperature: () => { delete body.temperature; },
126
137
  warnOnce: () => this._warnTemperatureDropped(),
127
138
  });
139
+ // BA-27: a successful 200 whose body carries no `choices` (some OpenAI-compat servers return a
140
+ // 4xx-shaped error object with HTTP 200) reached `data.choices[0]` as a bare TypeError with no
141
+ // context. Throw a ProviderError carrying the first ~300 bytes of the body so it can be told apart.
142
+ if (!Array.isArray(data.choices) || data.choices.length === 0) {
143
+ // The `context.bound:'no-choices'` marker ALWAYS distinguishes a 4xx-in-200 from other failures.
144
+ // The raw body snippet is gated behind `exposeErrorBody` (default off) like every other error path
145
+ // here — an unexpected field in a compat server's error body must not leak into logs/audit rows
146
+ // (err.message flows into Loop.run().error) unless the caller opts in.
147
+ throw new ProviderError(
148
+ `[OpenAIProvider] response has no choices` +
149
+ (this.exposeErrorBody ? `: ${JSON.stringify(data).slice(0, 300)}` : ''),
150
+ /** @type {any} */ ({ context: { bound: 'no-choices' }, body: this.exposeErrorBody ? data : undefined })
151
+ );
152
+ }
128
153
  const choice = data.choices[0];
129
154
  const msg = choice.message;
130
155
 
131
- /** @type {import('../types').ToolCall[]} */
132
- const toolCalls = (msg.tool_calls || []).map((/** @type {any} */ tc) => ({
156
+ // BA-27: `function.arguments` is a model-generated JSON STRING — a malformed one (extra brace,
157
+ // truncated object) must NOT throw here (the round already billed; a throw loses usage + hangs
158
+ // metering). parseToolCalls returns no usable calls + a marker; usage/model still flow below.
159
+ const { toolCalls, malformedToolCall } = parseToolCalls(msg.tool_calls, (/** @type {any} */ tc) => ({
133
160
  id: tc.id,
134
161
  name: tc.function.name,
135
162
  arguments: JSON.parse(tc.function.arguments),
@@ -138,6 +165,7 @@ class OpenAIProvider {
138
165
  return {
139
166
  text: msg.content || '',
140
167
  toolCalls,
168
+ ...(malformedToolCall && { malformedToolCall }),
141
169
  model: data.model || this.model,
142
170
  // BA-6: `length` ⇒ cut off at the output cap (normalized to 'max_tokens'). Note OpenAI refuses to
143
171
  // emit a tool call it cannot finish — it 400s instead — so a truncated round here carries no
@@ -0,0 +1,29 @@
1
+ export type ToolCall = import("../types").ToolCall;
2
+ /** @typedef {import('../types').ToolCall} ToolCall */
3
+ /**
4
+ * BA-27 — parse a round's raw tool calls into the neutral {@link ToolCall} shape WITHOUT throwing on
5
+ * malformed arguments.
6
+ *
7
+ * OpenAI-compatible providers return `function.arguments` as a JSON STRING the model generated, so a
8
+ * model that emits syntactically-broken JSON (an extra brace, a truncated object — seen live on
9
+ * deepseek-flash and other compat servers) makes a bare `JSON.parse` throw a `SyntaxError`. That throw
10
+ * lands AFTER the HTTP round already succeeded and `usage` came back, so it loses the billed round and
11
+ * hangs any metering, and no caller can tell "the model emitted bad arguments" from a transport fault.
12
+ *
13
+ * Instead: on the FIRST unparseable call, return NO usable tool calls (`toolCalls: []`) plus a marker
14
+ * `{ name, error }`. The caller treats it as "no usable tool call" and retries; usage/model still flow
15
+ * so the round is metered. We NEVER repair the JSON — a guessed brace could execute the wrong action.
16
+ * All-or-nothing (mirrors BA-4's refusal to execute a truncated round's calls): a partial set risks
17
+ * running half a decomposed intent, so one bad call voids the whole round's calls.
18
+ *
19
+ * @param {any[]} rawToolCalls - provider-native tool-call entries (may be undefined/empty)
20
+ * @param {(tc: any) => ToolCall} mapOne - maps one raw entry to a ToolCall; MAY throw on bad arguments
21
+ * @returns {{ toolCalls: ToolCall[], malformedToolCall?: { name: string|undefined, error: string } }}
22
+ */
23
+ export function parseToolCalls(rawToolCalls: any[], mapOne: (tc: any) => ToolCall): {
24
+ toolCalls: ToolCall[];
25
+ malformedToolCall?: {
26
+ name: string | undefined;
27
+ error: string;
28
+ };
29
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /** @typedef {import('../types').ToolCall} ToolCall */
4
+
5
+ /**
6
+ * BA-27 — parse a round's raw tool calls into the neutral {@link ToolCall} shape WITHOUT throwing on
7
+ * malformed arguments.
8
+ *
9
+ * OpenAI-compatible providers return `function.arguments` as a JSON STRING the model generated, so a
10
+ * model that emits syntactically-broken JSON (an extra brace, a truncated object — seen live on
11
+ * deepseek-flash and other compat servers) makes a bare `JSON.parse` throw a `SyntaxError`. That throw
12
+ * lands AFTER the HTTP round already succeeded and `usage` came back, so it loses the billed round and
13
+ * hangs any metering, and no caller can tell "the model emitted bad arguments" from a transport fault.
14
+ *
15
+ * Instead: on the FIRST unparseable call, return NO usable tool calls (`toolCalls: []`) plus a marker
16
+ * `{ name, error }`. The caller treats it as "no usable tool call" and retries; usage/model still flow
17
+ * so the round is metered. We NEVER repair the JSON — a guessed brace could execute the wrong action.
18
+ * All-or-nothing (mirrors BA-4's refusal to execute a truncated round's calls): a partial set risks
19
+ * running half a decomposed intent, so one bad call voids the whole round's calls.
20
+ *
21
+ * @param {any[]} rawToolCalls - provider-native tool-call entries (may be undefined/empty)
22
+ * @param {(tc: any) => ToolCall} mapOne - maps one raw entry to a ToolCall; MAY throw on bad arguments
23
+ * @returns {{ toolCalls: ToolCall[], malformedToolCall?: { name: string|undefined, error: string } }}
24
+ */
25
+ function parseToolCalls(rawToolCalls, mapOne) {
26
+ const raw = rawToolCalls || [];
27
+ /** @type {ToolCall[]} */
28
+ const toolCalls = [];
29
+ for (const tc of raw) {
30
+ try {
31
+ toolCalls.push(mapOne(tc));
32
+ } catch (e) {
33
+ return {
34
+ toolCalls: [],
35
+ malformedToolCall: {
36
+ name: tc && tc.function ? tc.function.name : undefined,
37
+ error: e instanceof Error ? e.message : String(e),
38
+ },
39
+ };
40
+ }
41
+ }
42
+ return { toolCalls };
43
+ }
44
+
45
+ module.exports = { parseToolCalls };
@@ -99,6 +99,12 @@ export function normalizeCorpus(corpus: unknown): Slice[];
99
99
  * @param {{recall: Function}} litectx
100
100
  * @param {{kinds?: string[], n?: number}} [opts]
101
101
  * @returns {ToolDef}
102
+ * @category integration
103
+ * @when you want to give a recurse worker a needle-search handle over litectx (embeddings recall for the relevant few) — for FINDING, never counting
104
+ * @fails returns matched bodies only and CANNOT count (the completeness guard blocks a "how many" ask before it is offered); a dead window yields nothing, never a fabricated hit.
105
+ * @example
106
+ * const tool = buildSearchTool(litectx, { n: 8 });
107
+ * await recurse(task, ctx, { retrieval: 'search', tools: [tool] });
102
108
  */
103
109
  export function buildSearchTool(litectx: {
104
110
  recall: Function;
@@ -114,6 +120,11 @@ export function buildSearchTool(litectx: {
114
120
  * OFF to stay exact — deferred; the code-side filter is the embeddings-free path shipped now.)
115
121
  * @param {Slice[]} corpus - The validated slice-source.
116
122
  * @returns {ToolDef}
123
+ * @when you want an embeddings-free, exact AND-term filter handle over a slice-source — complete over its slices, for precise lexical matches
124
+ * @fails only as good as a lexical rule; complete over the slices it is given (no recall cap) but matches nothing outside them.
125
+ * @example
126
+ * const tool = buildExactTool(corpus);
127
+ * await recurse(task, ctx, { retrieval: 'exact', tools: [tool] });
117
128
  */
118
129
  export function buildExactTool(corpus: Slice[]): ToolDef;
119
130
  /**
@@ -131,6 +142,11 @@ export function buildExactTool(corpus: Slice[]): ToolDef;
131
142
  * materialized lazily on first call and cached for the tool's lifetime.
132
143
  * @param {{provider: Provider, window?: number, passes?: number, ctx?: object, onLlmResult?: Function, policy?: Function}} opts
133
144
  * @returns {ToolDef}
145
+ * @when you need the complete count/"all" path — scan every slice + LLM-judge + code-count — the only retrieval mode that can honestly answer "how many"
146
+ * @fails a dead window surfaces as `INCOMPLETE — the count is a floor`, never a clean number over a hole; a governance HaltError propagates clean.
147
+ * @example
148
+ * const tool = buildScanTool(corpus, { provider, window: 8 });
149
+ * await recurse('how many mention X', ctx, { tools: [tool] });
134
150
  */
135
151
  export function buildScanTool(corpus: Slice[] | (() => Promise<Slice[]>), opts: {
136
152
  provider: Provider;
@@ -154,6 +170,12 @@ export function buildScanTool(corpus: Slice[] | (() => Promise<Slice[]>), opts:
154
170
  * @param {{enumerate: Function}} litectx
155
171
  * @param {{kind?: 'fact'|'episode', pageSize?: number}} [opts]
156
172
  * @returns {() => Promise<Slice[]>}
173
+ * @category integration
174
+ * @when you want a resident slice-source that paginates a litectx corpus via enumerate — for a corpus ALREADY in litectx, feeding scan/partition
175
+ * @fails never ingests a fresh corpus (strictly worse than scanning an in-hand array); returns an async source materialized once and cached.
176
+ * @example
177
+ * const corpus = litectxCorpus(litectx, { kind: 'fact' });
178
+ * await recurse(task, ctx, { mode: 'partition', corpus });
157
179
  */
158
180
  export function litectxCorpus(litectx: {
159
181
  enumerate: Function;
@@ -188,6 +188,12 @@ function normalizeCorpus(corpus) {
188
188
  * @param {{recall: Function}} litectx
189
189
  * @param {{kinds?: string[], n?: number}} [opts]
190
190
  * @returns {ToolDef}
191
+ * @category integration
192
+ * @when you want to give a recurse worker a needle-search handle over litectx (embeddings recall for the relevant few) — for FINDING, never counting
193
+ * @fails returns matched bodies only and CANNOT count (the completeness guard blocks a "how many" ask before it is offered); a dead window yields nothing, never a fabricated hit.
194
+ * @example
195
+ * const tool = buildSearchTool(litectx, { n: 8 });
196
+ * await recurse(task, ctx, { retrieval: 'search', tools: [tool] });
191
197
  */
192
198
  function buildSearchTool(litectx, opts = {}) {
193
199
  const kinds = Array.isArray(opts.kinds) && opts.kinds.length ? opts.kinds : ['fact', 'episode'];
@@ -224,6 +230,11 @@ function buildSearchTool(litectx, opts = {}) {
224
230
  * OFF to stay exact — deferred; the code-side filter is the embeddings-free path shipped now.)
225
231
  * @param {Slice[]} corpus - The validated slice-source.
226
232
  * @returns {ToolDef}
233
+ * @when you want an embeddings-free, exact AND-term filter handle over a slice-source — complete over its slices, for precise lexical matches
234
+ * @fails only as good as a lexical rule; complete over the slices it is given (no recall cap) but matches nothing outside them.
235
+ * @example
236
+ * const tool = buildExactTool(corpus);
237
+ * await recurse(task, ctx, { retrieval: 'exact', tools: [tool] });
227
238
  */
228
239
  function buildExactTool(corpus) {
229
240
  const slices = normalizeCorpus(corpus);
@@ -269,6 +280,11 @@ function buildExactTool(corpus) {
269
280
  * materialized lazily on first call and cached for the tool's lifetime.
270
281
  * @param {{provider: Provider, window?: number, passes?: number, ctx?: object, onLlmResult?: Function, policy?: Function}} opts
271
282
  * @returns {ToolDef}
283
+ * @when you need the complete count/"all" path — scan every slice + LLM-judge + code-count — the only retrieval mode that can honestly answer "how many"
284
+ * @fails a dead window surfaces as `INCOMPLETE — the count is a floor`, never a clean number over a hole; a governance HaltError propagates clean.
285
+ * @example
286
+ * const tool = buildScanTool(corpus, { provider, window: 8 });
287
+ * await recurse('how many mention X', ctx, { tools: [tool] });
272
288
  */
273
289
  function buildScanTool(corpus, opts) {
274
290
  /** @type {Slice[]|null} */
@@ -337,6 +353,12 @@ const ENUM_PAGE = 200;
337
353
  * @param {{enumerate: Function}} litectx
338
354
  * @param {{kind?: 'fact'|'episode', pageSize?: number}} [opts]
339
355
  * @returns {() => Promise<Slice[]>}
356
+ * @category integration
357
+ * @when you want a resident slice-source that paginates a litectx corpus via enumerate — for a corpus ALREADY in litectx, feeding scan/partition
358
+ * @fails never ingests a fresh corpus (strictly worse than scanning an in-hand array); returns an async source materialized once and cached.
359
+ * @example
360
+ * const corpus = litectxCorpus(litectx, { kind: 'fact' });
361
+ * await recurse(task, ctx, { mode: 'partition', corpus });
340
362
  */
341
363
  function litectxCorpus(litectx, opts = {}) {
342
364
  const kind = opts.kind === 'episode' ? 'episode' : 'fact'; // enumerate v1 is the memory axis (fact/episode)
package/src/recurse.d.ts CHANGED
@@ -604,5 +604,11 @@ export type Slice = {
604
604
  * @returns {Promise<RecurseResult>} `{ result, verdict, receipts }` on convergence; `{ incomplete, best,
605
605
  * receipts }` on guard exhaustion. NEVER a fabricated success (RC-9).
606
606
  * @throws {Error} no provider supplied (on neither `ctx.provider` nor `opts.provider`).
607
+ * @when a task is too big for one model pass and you want it split, fanned out, verified, and merged — with total cost capped by a gate
608
+ * @fails returns `{incomplete, best}` on guard exhaustion or a dead worker (never a faked pass); a gate HaltError exits clean. Cost is open by design — run under a budget gate.
609
+ * @example
610
+ * const ctx = wireGate(gate);
611
+ * const { result, incomplete } = await recurse('audit 400 logs', ctx, { provider, corpus });
612
+ * if (incomplete) retryOrEscalate(result);
607
613
  */
608
614
  export function recurse(task: string, ctx?: RecurseCtx, opts?: RecurseOptions): Promise<RecurseResult>;
package/src/recurse.js CHANGED
@@ -395,6 +395,12 @@ function auditSafeCtx(ctx, overrides = {}) {
395
395
  * @returns {Promise<RecurseResult>} `{ result, verdict, receipts }` on convergence; `{ incomplete, best,
396
396
  * receipts }` on guard exhaustion. NEVER a fabricated success (RC-9).
397
397
  * @throws {Error} no provider supplied (on neither `ctx.provider` nor `opts.provider`).
398
+ * @when a task is too big for one model pass and you want it split, fanned out, verified, and merged — with total cost capped by a gate
399
+ * @fails returns `{incomplete, best}` on guard exhaustion or a dead worker (never a faked pass); a gate HaltError exits clean. Cost is open by design — run under a budget gate.
400
+ * @example
401
+ * const ctx = wireGate(gate);
402
+ * const { result, incomplete } = await recurse('audit 400 logs', ctx, { provider, corpus });
403
+ * if (incomplete) retryOrEscalate(result);
398
404
  */
399
405
  async function recurse(task, ctx = {}, opts = {}) {
400
406
  if (typeof task !== 'string' || task.length === 0) {
package/src/refine.d.ts CHANGED
@@ -98,5 +98,10 @@ export type RefineOutcome = {
98
98
  *
99
99
  * @param {RefineOptions} options
100
100
  * @returns {Promise<RefineOutcome>}
101
+ * @when you have a caller-supplied attempt + evaluate pair and want to iterate generate → grade → regenerate until it passes or hits a bound
102
+ * @fails returns the last outcome on maxIterations or a terminal `failed` verdict (never a faked pass); a HaltError from either callback propagates clean.
103
+ * @example
104
+ * const { result, passed } = await refine({ attempt, evaluate, maxIterations: 3 });
105
+ * if (!passed) escalate(result);
101
106
  */
102
107
  export function refine(options: RefineOptions): Promise<RefineOutcome>;
package/src/refine.js CHANGED
@@ -38,6 +38,11 @@
38
38
  *
39
39
  * @param {RefineOptions} options
40
40
  * @returns {Promise<RefineOutcome>}
41
+ * @when you have a caller-supplied attempt + evaluate pair and want to iterate generate → grade → regenerate until it passes or hits a bound
42
+ * @fails returns the last outcome on maxIterations or a terminal `failed` verdict (never a faked pass); a HaltError from either callback propagates clean.
43
+ * @example
44
+ * const { result, passed } = await refine({ attempt, evaluate, maxIterations: 3 });
45
+ * if (!passed) escalate(result);
41
46
  */
42
47
  async function refine(options) {
43
48
  const { attempt, evaluate } = options;
package/src/remember.d.ts CHANGED
@@ -84,6 +84,11 @@ export type Store = import("../types").Store;
84
84
  * Each is a transcript chunk — a raw string, or an object with `content`/`text`. Empty/blank spans are skipped.
85
85
  * @param {RememberOptions} options
86
86
  * @returns {Promise<RememberOutcome>}
87
+ * @when you want to distill durable facts from harvested transcript spans and persist them through a Store socket (the consolidation pass)
88
+ * @fails skips empty spans and never fabricates; a provider HaltError propagates clean. Each pass forwards usage via onLlmResult; a fact counts once via ctx.recordMemoryOp.
89
+ * @example
90
+ * const { facts } = await remember(spans, { provider, store });
91
+ * console.log(`consolidated ${facts.length} durable facts`);
87
92
  */
88
93
  export function remember(spans: Array<string | {
89
94
  content?: string;
package/src/remember.js CHANGED
@@ -73,6 +73,11 @@ const DISTILL_PROMPT = [
73
73
  * Each is a transcript chunk — a raw string, or an object with `content`/`text`. Empty/blank spans are skipped.
74
74
  * @param {RememberOptions} options
75
75
  * @returns {Promise<RememberOutcome>}
76
+ * @when you want to distill durable facts from harvested transcript spans and persist them through a Store socket (the consolidation pass)
77
+ * @fails skips empty spans and never fabricates; a provider HaltError propagates clean. Each pass forwards usage via onLlmResult; a fact counts once via ctx.recordMemoryOp.
78
+ * @example
79
+ * const { facts } = await remember(spans, { provider, store });
80
+ * console.log(`consolidated ${facts.length} durable facts`);
76
81
  */
77
82
  async function remember(spans, options = /** @type {RememberOptions} */ ({})) {
78
83
  if (!Array.isArray(spans)) throw new Error('[remember] spans must be an array');
package/src/retry.d.ts CHANGED
@@ -21,7 +21,14 @@ export type RetryOptions = {
21
21
  jitter?: number | boolean | "full" | "equal" | undefined;
22
22
  };
23
23
  export class Retry {
24
- /** @param {RetryOptions} [options={}] */
24
+ /**
25
+ * @param {RetryOptions} [options={}]
26
+ * @when you want backoff-with-jitter around a flaky async call (a provider request, a plan step) — the retry seam Loop and runPlan wrap providers with
27
+ * @fails rethrows the last error once attempts are exhausted; by default only transient errors (429/5xx/ECONNRESET/ETIMEDOUT) are retried.
28
+ * @example
29
+ * const retry = new Retry({ maxAttempts: 3, jitter: true });
30
+ * const res = await retry.call(() => provider.generate(msgs));
31
+ */
25
32
  constructor(options?: RetryOptions);
26
33
  maxAttempts: number;
27
34
  backoff: number | "linear" | "exponential";
package/src/retry.js CHANGED
@@ -23,7 +23,14 @@ const DEFAULT_RETRY_ON = (err) => {
23
23
  };
24
24
 
25
25
  class Retry {
26
- /** @param {RetryOptions} [options={}] */
26
+ /**
27
+ * @param {RetryOptions} [options={}]
28
+ * @when you want backoff-with-jitter around a flaky async call (a provider request, a plan step) — the retry seam Loop and runPlan wrap providers with
29
+ * @fails rethrows the last error once attempts are exhausted; by default only transient errors (429/5xx/ECONNRESET/ETIMEDOUT) are retried.
30
+ * @example
31
+ * const retry = new Retry({ maxAttempts: 3, jitter: true });
32
+ * const res = await retry.call(() => provider.generate(msgs));
33
+ */
27
34
  constructor(options = {}) {
28
35
  this.maxAttempts = options.maxAttempts !== undefined ? options.maxAttempts : 3;
29
36
  this.backoff = options.backoff || 'exponential';
package/src/run-plan.d.ts CHANGED
@@ -122,5 +122,10 @@ export type StepResult = {
122
122
  * @throws {Error} `[runPlan] executeFn must be a function` — when executeFn is not a function.
123
123
  * @throws {Error} `[runPlan] duplicate step id: "X"` — when two steps share an id.
124
124
  * @throws {Error} `[runPlan] step "X" depends on unknown step "Y"` — when dependsOn references missing id.
125
+ * @when you have a step DAG from the Planner and want to execute it with wave-based parallelism (independent steps run concurrently)
126
+ * @fails throws on a malformed DAG (empty steps, non-function executeFn, duplicate ids, unknown dependency); a step's own error surfaces per StepResult, never crashing the wave.
127
+ * @example
128
+ * const steps = await planner.plan(goal);
129
+ * const results = await runPlan(steps, step => execute(step));
125
130
  */
126
131
  export function runPlan(steps: Step[], executeFn: (step: Step) => any, options?: RunPlanOptions): Promise<StepResult[]>;
package/src/run-plan.js CHANGED
@@ -47,6 +47,11 @@
47
47
  * @throws {Error} `[runPlan] executeFn must be a function` — when executeFn is not a function.
48
48
  * @throws {Error} `[runPlan] duplicate step id: "X"` — when two steps share an id.
49
49
  * @throws {Error} `[runPlan] step "X" depends on unknown step "Y"` — when dependsOn references missing id.
50
+ * @when you have a step DAG from the Planner and want to execute it with wave-based parallelism (independent steps run concurrently)
51
+ * @fails throws on a malformed DAG (empty steps, non-function executeFn, duplicate ids, unknown dependency); a step's own error surfaces per StepResult, never crashing the wave.
52
+ * @example
53
+ * const steps = await planner.plan(goal);
54
+ * const results = await runPlan(steps, step => execute(step));
50
55
  */
51
56
  async function runPlan(steps, executeFn, options = {}) {
52
57
  if (!Array.isArray(steps) || steps.length === 0) {
@@ -48,7 +48,14 @@ export type SchedulerOptions = {
48
48
  * @property {((err: any, job: Job) => void)|null} [onError] - Handler errors callback.
49
49
  */
50
50
  export class Scheduler {
51
- /** @param {SchedulerOptions} [options={}] */
51
+ /**
52
+ * @param {SchedulerOptions} [options={}]
53
+ * @when you need to fire agent turns on a schedule — cron expressions or relative intervals — driven by a periodic tick
54
+ * @fails an errored job routes to the onError handler and never crashes the tick loop; a malformed cron/interval is rejected when the job is added.
55
+ * @example
56
+ * const sched = new Scheduler({ interval: 60000 });
57
+ * sched.add({ id: 'poll', cron: '0 * * * *', run });
58
+ */
52
59
  constructor(options?: SchedulerOptions);
53
60
  _file: string | null;
54
61
  _interval: number;
package/src/scheduler.js CHANGED
@@ -32,7 +32,14 @@ const { readFileSync, writeFileSync, existsSync } = require('node:fs');
32
32
  */
33
33
 
34
34
  class Scheduler {
35
- /** @param {SchedulerOptions} [options={}] */
35
+ /**
36
+ * @param {SchedulerOptions} [options={}]
37
+ * @when you need to fire agent turns on a schedule — cron expressions or relative intervals — driven by a periodic tick
38
+ * @fails an errored job routes to the onError handler and never crashes the tick loop; a malformed cron/interval is rejected when the job is added.
39
+ * @example
40
+ * const sched = new Scheduler({ interval: 60000 });
41
+ * sched.add({ id: 'poll', cron: '0 * * * *', run });
42
+ */
36
43
  constructor(options = {}) {
37
44
  this._file = options.file || null;
38
45
  this._interval = options.interval || 60000;
package/src/skills.d.ts CHANGED
@@ -24,6 +24,12 @@ export class SkillRegistry {
24
24
  * skill tool that would collide with one is rejected at `register` time. Tool names are globally unique
25
25
  * for DISPATCH (PRD §2.6, D6) — this is the collision check across native + MCP + skills, not security.
26
26
  * @param {string} [options.metaToolName='skill_use'] - Override the meta-tool name if `skill_use` is taken.
27
+ * @when you want to expose operator-registered skill bundles to a model by progressive disclosure — one meta-tool whose catalog unlocks a skill's tools on demand
28
+ * @fails register() rejects an unsafe or colliding name fail-fast and commits nothing on failure; governance is unchanged — discovery never authorizes.
29
+ * @example
30
+ * const skills = new SkillRegistry();
31
+ * skills.register({ name: 'deploy', description: 'ship a release', instructions: '...', tools: [] });
32
+ * const loop = new Loop({ provider, tools: skills.activeTools });
27
33
  */
28
34
  constructor(options?: {
29
35
  reserved?: Iterable<string> | undefined;
package/src/skills.js CHANGED
@@ -51,6 +51,12 @@ class SkillRegistry {
51
51
  * skill tool that would collide with one is rejected at `register` time. Tool names are globally unique
52
52
  * for DISPATCH (PRD §2.6, D6) — this is the collision check across native + MCP + skills, not security.
53
53
  * @param {string} [options.metaToolName='skill_use'] - Override the meta-tool name if `skill_use` is taken.
54
+ * @when you want to expose operator-registered skill bundles to a model by progressive disclosure — one meta-tool whose catalog unlocks a skill's tools on demand
55
+ * @fails register() rejects an unsafe or colliding name fail-fast and commits nothing on failure; governance is unchanged — discovery never authorizes.
56
+ * @example
57
+ * const skills = new SkillRegistry();
58
+ * skills.register({ name: 'deploy', description: 'ship a release', instructions: '...', tools: [] });
59
+ * const loop = new Loop({ provider, tools: skills.activeTools });
54
60
  */
55
61
  constructor(options = {}) {
56
62
  /** @type {Map<string, {name: string, description: string, instructions: string, tools: ToolDef[]}>} */
package/src/stash.d.ts CHANGED
@@ -19,6 +19,11 @@ export type ToolDef = import("../types").ToolDef;
19
19
  * @param {number} [options.compaction.keepRecentTurns=3] - Recent turns to keep at the END (live working set).
20
20
  * @param {(msg: string) => void} [options.onNote=console.warn] - Sink for the loud one-time/backstop notes.
21
21
  * @returns {{ skill: { name: string, description: string, instructions: string, tools: ToolDef[] }, trim: (msgs: any[], ctx: any) => Promise<any[]>, restoreHandles: () => string[] }}
22
+ * @when you need compaction-first context hygiene — a registrable skill whose checkpoint/compact/restore tools fold the live transcript at round boundaries
23
+ * @fails never throws for a fold; degrades LOUDLY to a lossless park when summarize is unwired. Preserves tool-pairing and role alternation by construction.
24
+ * @example
25
+ * const { skill, trim } = createStashSkill({ compaction: { ceilingTokens: 100000 } });
26
+ * const loop = new Loop({ provider, trim });
22
27
  */
23
28
  export function createStashSkill(options?: {
24
29
  defaultStrategy?: "summarize" | "stash" | undefined;
package/src/stash.js CHANGED
@@ -82,6 +82,11 @@ const INSTRUCTIONS =
82
82
  * @param {number} [options.compaction.keepRecentTurns=3] - Recent turns to keep at the END (live working set).
83
83
  * @param {(msg: string) => void} [options.onNote=console.warn] - Sink for the loud one-time/backstop notes.
84
84
  * @returns {{ skill: { name: string, description: string, instructions: string, tools: ToolDef[] }, trim: (msgs: any[], ctx: any) => Promise<any[]>, restoreHandles: () => string[] }}
85
+ * @when you need compaction-first context hygiene — a registrable skill whose checkpoint/compact/restore tools fold the live transcript at round boundaries
86
+ * @fails never throws for a fold; degrades LOUDLY to a lossless park when summarize is unwired. Preserves tool-pairing and role alternation by construction.
87
+ * @example
88
+ * const { skill, trim } = createStashSkill({ compaction: { ceilingTokens: 100000 } });
89
+ * const loop = new Loop({ provider, trim });
85
90
  */
86
91
  function createStashSkill(options = {}) {
87
92
  const {
package/src/state.d.ts CHANGED
@@ -12,7 +12,14 @@ export type Task = {
12
12
  * @property {string} updatedAt
13
13
  */
14
14
  export class StateMachine extends EventEmitter<[never]> {
15
- /** @param {{ file?: string|null }} [options={}] */
15
+ /**
16
+ * @param {{ file?: string|null }} [options={}]
17
+ * @when you need to track task lifecycle (pending/running/done/failed/waiting/cancelled) with enforced transitions and optional file persistence
18
+ * @fails rejects an illegal state transition and never throws on a valid one; state changes are emitted as events.
19
+ * @example
20
+ * const sm = new StateMachine();
21
+ * sm.create('t1'); sm.transition('t1', 'running');
22
+ */
16
23
  constructor(options?: {
17
24
  file?: string | null;
18
25
  });
package/src/state.js CHANGED
@@ -21,7 +21,14 @@ const TRANSITIONS = {
21
21
  */
22
22
 
23
23
  class StateMachine extends EventEmitter {
24
- /** @param {{ file?: string|null }} [options={}] */
24
+ /**
25
+ * @param {{ file?: string|null }} [options={}]
26
+ * @when you need to track task lifecycle (pending/running/done/failed/waiting/cancelled) with enforced transitions and optional file persistence
27
+ * @fails rejects an illegal state transition and never throws on a valid one; state changes are emitted as events.
28
+ * @example
29
+ * const sm = new StateMachine();
30
+ * sm.create('t1'); sm.transition('t1', 'running');
31
+ */
25
32
  constructor(options = {}) {
26
33
  super();
27
34
  this.file = options.file || null;
@@ -42,6 +42,11 @@ export class JsonFileStore {
42
42
  /**
43
43
  * @param {{ path?: string }} [options]
44
44
  * @throws {Error} `[JsonFileStore] requires options.path` — when path is missing.
45
+ * @name JsonFile
46
+ * @when you want zero-dependency JSON-file storage for Memory (store/search/get/delete) — the simplest durable backend, no native deps
47
+ * @fails throws on a missing path; implements the four-verb Store socket over a plain JSON file.
48
+ * @example
49
+ * const store = new JsonFile({ path: './agent.json' });
45
50
  */
46
51
  constructor(options?: {
47
52
  path?: string;
@@ -31,6 +31,11 @@ class JsonFileStore {
31
31
  /**
32
32
  * @param {{ path?: string }} [options]
33
33
  * @throws {Error} `[JsonFileStore] requires options.path` — when path is missing.
34
+ * @name JsonFile
35
+ * @when you want zero-dependency JSON-file storage for Memory (store/search/get/delete) — the simplest durable backend, no native deps
36
+ * @fails throws on a missing path; implements the four-verb Store socket over a plain JSON file.
37
+ * @example
38
+ * const store = new JsonFile({ path: './agent.json' });
34
39
  */
35
40
  constructor(options = {}) {
36
41
  if (!options.path) throw new Error('[JsonFileStore] requires options.path');
@@ -43,6 +43,11 @@ export class SQLiteStore {
43
43
  * @param {{ path?: string }} [options]
44
44
  * @throws {Error} `[SQLiteStore] requires options.path` — when path is missing.
45
45
  * @throws {Error} `[SQLiteStore] requires better-sqlite3` — when peer dep is not installed.
46
+ * @name SQLite
47
+ * @when you want durable, queryable SQLite-backed storage for Memory (store/search/get/delete) persisted on disk
48
+ * @fails throws on a missing path or an absent better-sqlite3 peer dep; implements the four-verb Store socket.
49
+ * @example
50
+ * const store = new SQLite({ path: './agent.db' });
46
51
  */
47
52
  constructor(options?: {
48
53
  path?: string;
@@ -23,6 +23,11 @@ class SQLiteStore {
23
23
  * @param {{ path?: string }} [options]
24
24
  * @throws {Error} `[SQLiteStore] requires options.path` — when path is missing.
25
25
  * @throws {Error} `[SQLiteStore] requires better-sqlite3` — when peer dep is not installed.
26
+ * @name SQLite
27
+ * @when you want durable, queryable SQLite-backed storage for Memory (store/search/get/delete) persisted on disk
28
+ * @fails throws on a missing path or an absent better-sqlite3 peer dep; implements the four-verb Store socket.
29
+ * @example
30
+ * const store = new SQLite({ path: './agent.db' });
26
31
  */
27
32
  constructor(options = {}) {
28
33
  if (!options.path) throw new Error('[SQLiteStore] requires options.path');
package/src/stream.d.ts CHANGED
@@ -60,6 +60,11 @@ export type StreamOptions = {
60
60
  export class Stream {
61
61
  /**
62
62
  * @param {StreamOptions} [options={}]
63
+ * @when you want a structured event emitter for loop/tool/governance events, optionally piped to a transport sink
64
+ * @fails never throws on emit; a transport write error is isolated and does not interrupt the run.
65
+ * @example
66
+ * const stream = new Stream({ transport });
67
+ * stream.emit('loop:round', { n: 1 });
63
68
  */
64
69
  constructor(options?: StreamOptions);
65
70
  /** @type {Transport|null} */