bare-agent 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +1 -0
  2. package/bareagent.context.md +1149 -0
  3. package/bin/cli.d.ts +4 -0
  4. package/bin/cli.js +40 -10
  5. package/bin/test-provider.d.ts +2 -0
  6. package/bin/test-provider.js +5 -1
  7. package/index.d.ts +20 -0
  8. package/package.json +46 -10
  9. package/src/bareguard-adapter.d.ts +118 -0
  10. package/src/bareguard-adapter.js +75 -3
  11. package/src/checkpoint.d.ts +61 -0
  12. package/src/checkpoint.js +17 -8
  13. package/src/circuit-breaker.d.ts +70 -0
  14. package/src/circuit-breaker.js +20 -4
  15. package/src/errors.d.ts +106 -0
  16. package/src/errors.js +50 -1
  17. package/src/loop.d.ts +135 -0
  18. package/src/loop.js +73 -17
  19. package/src/mcp-bridge.d.ts +133 -0
  20. package/src/mcp-bridge.js +179 -27
  21. package/src/mcp.d.ts +4 -0
  22. package/src/memory.d.ts +50 -0
  23. package/src/memory.js +22 -2
  24. package/src/planner.d.ts +62 -0
  25. package/src/planner.js +26 -7
  26. package/src/provider-anthropic.d.ts +55 -0
  27. package/src/provider-anthropic.js +32 -11
  28. package/src/provider-clipipe.d.ts +86 -0
  29. package/src/provider-clipipe.js +28 -18
  30. package/src/provider-fallback.d.ts +44 -0
  31. package/src/provider-fallback.js +18 -8
  32. package/src/provider-ollama.d.ts +41 -0
  33. package/src/provider-ollama.js +27 -7
  34. package/src/provider-openai.d.ts +57 -0
  35. package/src/provider-openai.js +31 -16
  36. package/src/providers.d.ts +6 -0
  37. package/src/providers.js +8 -0
  38. package/src/retry.d.ts +44 -0
  39. package/src/retry.js +15 -1
  40. package/src/run-plan.d.ts +126 -0
  41. package/src/run-plan.js +46 -13
  42. package/src/scheduler.d.ts +102 -0
  43. package/src/scheduler.js +32 -4
  44. package/src/state.d.ts +45 -0
  45. package/src/state.js +18 -2
  46. package/src/store-jsonfile.d.ts +85 -0
  47. package/src/store-jsonfile.js +33 -8
  48. package/src/store-sqlite.d.ts +90 -0
  49. package/src/store-sqlite.js +31 -7
  50. package/src/stores.d.ts +3 -0
  51. package/src/stream.d.ts +79 -0
  52. package/src/stream.js +32 -0
  53. package/src/tools.d.ts +8 -0
  54. package/src/transport-jsonl.d.ts +30 -0
  55. package/src/transport-jsonl.js +13 -0
  56. package/src/transports.d.ts +2 -0
  57. package/tools/browse.d.ts +10 -0
  58. package/tools/browse.js +2 -0
  59. package/tools/defer.d.ts +33 -0
  60. package/tools/defer.js +12 -3
  61. package/tools/mobile.d.ts +34 -0
  62. package/tools/mobile.js +28 -15
  63. package/tools/shell.d.ts +31 -0
  64. package/tools/shell.js +55 -6
  65. package/tools/spawn.d.ts +107 -0
  66. package/tools/spawn.js +24 -5
  67. package/types/index.d.ts +66 -0
  68. package/types/shims.d.ts +16 -0
@@ -0,0 +1,70 @@
1
+ export type CircuitState = "closed" | "open" | "half-open";
2
+ export type CircuitEntry = {
3
+ state: CircuitState;
4
+ failures: number;
5
+ openedAt: number;
6
+ generation: number;
7
+ };
8
+ /**
9
+ * @typedef {'closed'|'open'|'half-open'} CircuitState
10
+ * @typedef {{ state: CircuitState, failures: number, openedAt: number, generation: number }} CircuitEntry
11
+ */
12
+ export class CircuitBreaker {
13
+ /**
14
+ * @param {object} [options={}]
15
+ * @param {number} [options.threshold=5] - Failures before opening.
16
+ * @param {number} [options.resetAfter=60000] - Ms before half-open probe.
17
+ * @param {((key: string, from: CircuitState, to: CircuitState) => void)} [options.onStateChange] - Callback(key, from, to).
18
+ */
19
+ constructor(options?: {
20
+ threshold?: number | undefined;
21
+ resetAfter?: number | undefined;
22
+ onStateChange?: ((key: string, from: CircuitState, to: CircuitState) => void) | undefined;
23
+ });
24
+ threshold: number;
25
+ resetAfter: number;
26
+ onStateChange: ((key: string, from: CircuitState, to: CircuitState) => void) | null;
27
+ /** @type {Map<string, CircuitEntry>} */
28
+ _keys: Map<string, CircuitEntry>;
29
+ /**
30
+ * @param {string} key
31
+ * @returns {CircuitEntry}
32
+ */
33
+ _getEntry(key: string): CircuitEntry;
34
+ /**
35
+ * @param {CircuitEntry} entry
36
+ * @param {string} key
37
+ * @param {CircuitState} newState
38
+ */
39
+ _setState(entry: CircuitEntry, key: string, newState: CircuitState): void;
40
+ /**
41
+ * Execute fn through the circuit breaker.
42
+ * @param {function} fn - Async function to call.
43
+ * @param {string} [key='default'] - Circuit key for per-key isolation.
44
+ * @returns {Promise<*>}
45
+ * @throws {CircuitOpenError} When circuit is open.
46
+ */
47
+ call(fn: Function, key?: string): Promise<any>;
48
+ /**
49
+ * Get current state for a key.
50
+ * @param {string} [key='default']
51
+ * @returns {'closed'|'open'|'half-open'}
52
+ */
53
+ getState(key?: string): "closed" | "open" | "half-open";
54
+ /**
55
+ * Force reset a key to closed.
56
+ * @param {string} [key='default']
57
+ */
58
+ reset(key?: string): void;
59
+ /**
60
+ * Wrap a provider so generate() goes through the circuit breaker.
61
+ * @param {{ generate: (...args: any[]) => Promise<any> }} provider - Provider with generate().
62
+ * @param {string} [key] - Circuit key.
63
+ * @returns {{ generate: (...args: any[]) => Promise<any> }} Wrapped provider with generate().
64
+ */
65
+ wrapProvider(provider: {
66
+ generate: (...args: any[]) => Promise<any>;
67
+ }, key?: string): {
68
+ generate: (...args: any[]) => Promise<any>;
69
+ };
70
+ }
@@ -2,27 +2,42 @@
2
2
 
3
3
  const { CircuitOpenError } = require('./errors');
4
4
 
5
+ /**
6
+ * @typedef {'closed'|'open'|'half-open'} CircuitState
7
+ * @typedef {{ state: CircuitState, failures: number, openedAt: number, generation: number }} CircuitEntry
8
+ */
9
+
5
10
  class CircuitBreaker {
6
11
  /**
7
12
  * @param {object} [options={}]
8
13
  * @param {number} [options.threshold=5] - Failures before opening.
9
14
  * @param {number} [options.resetAfter=60000] - Ms before half-open probe.
10
- * @param {function} [options.onStateChange] - Callback(key, from, to).
15
+ * @param {((key: string, from: CircuitState, to: CircuitState) => void)} [options.onStateChange] - Callback(key, from, to).
11
16
  */
12
17
  constructor(options = {}) {
13
18
  this.threshold = options.threshold || 5;
14
19
  this.resetAfter = options.resetAfter || 60000;
15
20
  this.onStateChange = options.onStateChange || null;
21
+ /** @type {Map<string, CircuitEntry>} */
16
22
  this._keys = new Map();
17
23
  }
18
24
 
25
+ /**
26
+ * @param {string} key
27
+ * @returns {CircuitEntry}
28
+ */
19
29
  _getEntry(key) {
20
30
  if (!this._keys.has(key)) {
21
31
  this._keys.set(key, { state: 'closed', failures: 0, openedAt: 0, generation: 0 });
22
32
  }
23
- return this._keys.get(key);
33
+ return /** @type {CircuitEntry} */ (this._keys.get(key));
24
34
  }
25
35
 
36
+ /**
37
+ * @param {CircuitEntry} entry
38
+ * @param {string} key
39
+ * @param {CircuitState} newState
40
+ */
26
41
  _setState(entry, key, newState) {
27
42
  const from = entry.state;
28
43
  if (from === newState) return;
@@ -98,12 +113,13 @@ class CircuitBreaker {
98
113
 
99
114
  /**
100
115
  * Wrap a provider so generate() goes through the circuit breaker.
101
- * @param {object} provider - Provider with generate().
116
+ * @param {{ generate: (...args: any[]) => Promise<any> }} provider - Provider with generate().
102
117
  * @param {string} [key] - Circuit key.
103
- * @returns {object} Wrapped provider with generate().
118
+ * @returns {{ generate: (...args: any[]) => Promise<any> }} Wrapped provider with generate().
104
119
  */
105
120
  wrapProvider(provider, key) {
106
121
  return {
122
+ /** @param {...any} args */
107
123
  generate: (...args) => this.call(() => provider.generate(...args), key),
108
124
  };
109
125
  }
@@ -0,0 +1,106 @@
1
+ export type BareAgentErrorOptions = {
2
+ /**
3
+ * - Stable error code (e.g. 'PROVIDER_ERROR').
4
+ */
5
+ code?: string | undefined;
6
+ /**
7
+ * - Whether the operation may be safely retried.
8
+ */
9
+ retryable?: boolean | undefined;
10
+ /**
11
+ * - Arbitrary structured context.
12
+ */
13
+ context?: Record<string, any> | undefined;
14
+ };
15
+ export type ProviderErrorOptions = {
16
+ /**
17
+ * - HTTP status from the provider.
18
+ */
19
+ status?: number | undefined;
20
+ /**
21
+ * - Raw response body.
22
+ */
23
+ body?: any;
24
+ /**
25
+ * - Arbitrary structured context.
26
+ */
27
+ context?: Record<string, any> | undefined;
28
+ };
29
+ export type HaltErrorOptions = {
30
+ /**
31
+ * - The bareguard rule that triggered the halt.
32
+ */
33
+ rule?: string | undefined;
34
+ /**
35
+ * - The full bareguard decision object.
36
+ */
37
+ decision?: any;
38
+ /**
39
+ * - Arbitrary structured context.
40
+ */
41
+ context?: Record<string, any> | undefined;
42
+ };
43
+ /**
44
+ * @typedef {object} BareAgentErrorOptions
45
+ * @property {string} [code] - Stable error code (e.g. 'PROVIDER_ERROR').
46
+ * @property {boolean} [retryable] - Whether the operation may be safely retried.
47
+ * @property {Record<string, any>} [context] - Arbitrary structured context.
48
+ */
49
+ /**
50
+ * @typedef {object} ProviderErrorOptions
51
+ * @property {number} [status] - HTTP status from the provider.
52
+ * @property {any} [body] - Raw response body.
53
+ * @property {Record<string, any>} [context] - Arbitrary structured context.
54
+ */
55
+ /**
56
+ * @typedef {object} HaltErrorOptions
57
+ * @property {string} [rule] - The bareguard rule that triggered the halt.
58
+ * @property {any} [decision] - The full bareguard decision object.
59
+ * @property {Record<string, any>} [context] - Arbitrary structured context.
60
+ */
61
+ export class BareAgentError extends Error {
62
+ /**
63
+ * @param {string} message
64
+ * @param {BareAgentErrorOptions} [options]
65
+ */
66
+ constructor(message: string, { code, retryable, context }?: BareAgentErrorOptions);
67
+ code: string | undefined;
68
+ retryable: boolean;
69
+ context: Record<string, any>;
70
+ }
71
+ export class ProviderError extends BareAgentError {
72
+ /**
73
+ * @param {string} message
74
+ * @param {ProviderErrorOptions} [options]
75
+ */
76
+ constructor(message: string, { status, body, context }?: ProviderErrorOptions);
77
+ status: number | undefined;
78
+ body: any;
79
+ }
80
+ export class ToolError extends BareAgentError {
81
+ }
82
+ export class TimeoutError extends BareAgentError {
83
+ /**
84
+ * @param {string} [message]
85
+ * @param {BareAgentErrorOptions} [opts]
86
+ */
87
+ constructor(message?: string, opts?: BareAgentErrorOptions);
88
+ }
89
+ export class ValidationError extends BareAgentError {
90
+ }
91
+ export class CircuitOpenError extends BareAgentError {
92
+ /**
93
+ * @param {string} [message]
94
+ * @param {BareAgentErrorOptions} [opts]
95
+ */
96
+ constructor(message?: string, opts?: BareAgentErrorOptions);
97
+ }
98
+ export class HaltError extends BareAgentError {
99
+ /**
100
+ * @param {string} [message]
101
+ * @param {HaltErrorOptions} [options]
102
+ */
103
+ constructor(message?: string, { rule, decision, context }?: HaltErrorOptions);
104
+ rule: string | null;
105
+ decision: any;
106
+ }
package/src/errors.js CHANGED
@@ -1,6 +1,31 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * @typedef {object} BareAgentErrorOptions
5
+ * @property {string} [code] - Stable error code (e.g. 'PROVIDER_ERROR').
6
+ * @property {boolean} [retryable] - Whether the operation may be safely retried.
7
+ * @property {Record<string, any>} [context] - Arbitrary structured context.
8
+ */
9
+
10
+ /**
11
+ * @typedef {object} ProviderErrorOptions
12
+ * @property {number} [status] - HTTP status from the provider.
13
+ * @property {any} [body] - Raw response body.
14
+ * @property {Record<string, any>} [context] - Arbitrary structured context.
15
+ */
16
+
17
+ /**
18
+ * @typedef {object} HaltErrorOptions
19
+ * @property {string} [rule] - The bareguard rule that triggered the halt.
20
+ * @property {any} [decision] - The full bareguard decision object.
21
+ * @property {Record<string, any>} [context] - Arbitrary structured context.
22
+ */
23
+
3
24
  class BareAgentError extends Error {
25
+ /**
26
+ * @param {string} message
27
+ * @param {BareAgentErrorOptions} [options]
28
+ */
4
29
  constructor(message, { code, retryable = false, context = {} } = {}) {
5
30
  super(message);
6
31
  this.name = this.constructor.name;
@@ -11,8 +36,12 @@ class BareAgentError extends Error {
11
36
  }
12
37
 
13
38
  class ProviderError extends BareAgentError {
39
+ /**
40
+ * @param {string} message
41
+ * @param {ProviderErrorOptions} [options]
42
+ */
14
43
  constructor(message, { status, body, context = {} } = {}) {
15
- const retryable = status === 429 || (status >= 500 && status <= 504);
44
+ const retryable = status === 429 || (status != null && status >= 500 && status <= 504);
16
45
  super(message, { code: 'PROVIDER_ERROR', retryable, context });
17
46
  this.status = status;
18
47
  this.body = body;
@@ -20,24 +49,40 @@ class ProviderError extends BareAgentError {
20
49
  }
21
50
 
22
51
  class ToolError extends BareAgentError {
52
+ /**
53
+ * @param {string} message
54
+ * @param {BareAgentErrorOptions} [opts]
55
+ */
23
56
  constructor(message, opts = {}) {
24
57
  super(message, { code: 'TOOL_ERROR', retryable: false, ...opts });
25
58
  }
26
59
  }
27
60
 
28
61
  class TimeoutError extends BareAgentError {
62
+ /**
63
+ * @param {string} [message]
64
+ * @param {BareAgentErrorOptions} [opts]
65
+ */
29
66
  constructor(message, opts = {}) {
30
67
  super(message || 'Operation timed out', { code: 'ETIMEDOUT', retryable: true, ...opts });
31
68
  }
32
69
  }
33
70
 
34
71
  class ValidationError extends BareAgentError {
72
+ /**
73
+ * @param {string} message
74
+ * @param {BareAgentErrorOptions} [opts]
75
+ */
35
76
  constructor(message, opts = {}) {
36
77
  super(message, { code: 'VALIDATION_ERROR', retryable: false, ...opts });
37
78
  }
38
79
  }
39
80
 
40
81
  class CircuitOpenError extends BareAgentError {
82
+ /**
83
+ * @param {string} [message]
84
+ * @param {BareAgentErrorOptions} [opts]
85
+ */
41
86
  constructor(message, opts = {}) {
42
87
  super(message || 'Circuit breaker is open', { code: 'CIRCUIT_OPEN', retryable: true, ...opts });
43
88
  }
@@ -48,6 +93,10 @@ class CircuitOpenError extends BareAgentError {
48
93
  // Loop's outer handler — does NOT propagate to the LLM as a tool result.
49
94
  // Loop exits cleanly: emits loop:error{source:'halt'} + loop:done, calls onError.
50
95
  class HaltError extends BareAgentError {
96
+ /**
97
+ * @param {string} [message]
98
+ * @param {HaltErrorOptions} [options]
99
+ */
51
100
  constructor(message, { rule, decision, context = {} } = {}) {
52
101
  super(message || `[HALT: ${rule || 'unknown'}]`, {
53
102
  code: 'HALT',
package/src/loop.d.ts ADDED
@@ -0,0 +1,135 @@
1
+ export type Provider = import("../types").Provider;
2
+ export type Message = import("../types").Message;
3
+ export type ToolDef = import("../types").ToolDef;
4
+ export type ToolCall = import("../types").ToolCall;
5
+ export type Usage = import("../types").Usage;
6
+ export type GenerateResult = import("../types").GenerateResult;
7
+ export type Store = import("../types").Store;
8
+ export type Checkpoint = import("./checkpoint").Checkpoint;
9
+ export type Retry = import("./retry").Retry;
10
+ export type Stream = import("./stream").Stream;
11
+ export type LoopOptions = {
12
+ provider: Provider;
13
+ system?: string | undefined;
14
+ checkpoint?: import("./checkpoint").Checkpoint | undefined;
15
+ retry?: import("./retry").Retry | undefined;
16
+ stream?: import("./stream").Stream | undefined;
17
+ store?: import("../types").Store | undefined;
18
+ onToolCall?: Function | undefined;
19
+ onText?: Function | undefined;
20
+ onError?: Function | undefined;
21
+ throwOnError?: boolean | undefined;
22
+ policy?: Function | undefined;
23
+ onLlmResult?: Function | undefined;
24
+ onToolResult?: Function | undefined;
25
+ /**
26
+ * - Removed in v0.8; presence throws a migration error.
27
+ */
28
+ maxRounds?: number | undefined;
29
+ };
30
+ export class Loop {
31
+ /**
32
+ * `policy` is async `(toolName, args, ctx) => true | string`. Recommended wiring: a closure
33
+ * that delegates to a bareguard Gate (`require('bare-agent/bareguard').wireGate(gate).policy`).
34
+ * Anything other than `true` denies; a string is fed to the LLM verbatim as the deny reason.
35
+ * A throw of `HaltError` exits the loop cleanly. `onLlmResult`/`onToolResult` forward usage and
36
+ * tool outcomes to `gate.record` (via wireGate) and never kill the loop on error.
37
+ * @param {LoopOptions} options
38
+ * @throws {Error} `[Loop] requires a provider` — when options.provider is missing.
39
+ */
40
+ constructor(options?: LoopOptions);
41
+ provider: import("../types").Provider;
42
+ system: string | null;
43
+ checkpoint: import("./checkpoint").Checkpoint | null;
44
+ retry: import("./retry").Retry | null;
45
+ stream: import("./stream").Stream | null;
46
+ onToolCall: Function | null;
47
+ onText: Function | null;
48
+ onError: Function | null;
49
+ throwOnError: boolean;
50
+ store: import("../types").Store | null;
51
+ policy: Function | null;
52
+ onLlmResult: Function | null;
53
+ onToolResult: Function | null;
54
+ _stopped: boolean;
55
+ /** @type {Message[]} */
56
+ _history: Message[];
57
+ /**
58
+ * @param {string} source
59
+ * @param {any} err
60
+ * @param {Record<string, any>} [extra]
61
+ */
62
+ _reportError(source: string, err: any, extra?: Record<string, any>): void;
63
+ /** @param {{type: string, data?: any, ts?: string}} event */
64
+ _safeEmit(event: {
65
+ type: string;
66
+ data?: any;
67
+ ts?: string;
68
+ }): void;
69
+ /**
70
+ * @param {string} name
71
+ * @param {Function|null} fn
72
+ * @param {...any} args
73
+ */
74
+ _safeCall(name: string, fn: Function | null, ...args: any[]): void;
75
+ /**
76
+ * Run the think/act/observe loop.
77
+ * @param {Message[]} messages - Conversation messages in OpenAI format.
78
+ * @param {ToolDef[]} [tools=[]] - Tool definitions with name, execute, description, parameters.
79
+ * @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
80
+ * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[]}>}
81
+ * On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
82
+ * thrown HaltError carried no `rule`), and `msgs` is sanitized so any
83
+ * dangling assistant `tool_calls` from the halted round are paired with
84
+ * synthetic `[halted]` tool replies — safe to feed back into another
85
+ * provider call without violating OpenAI's tool-call/tool-result pairing.
86
+ * @throws {Error} `[Loop] Tool is missing a name` — when a tool has no name or a non-string name.
87
+ * @throws {Error} `[Loop] Tool "X" is missing an execute() function` — when execute is not a function.
88
+ * @throws {Error} `[Loop] Tool "X" has invalid parameters` — when parameters is not an object.
89
+ */
90
+ run(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<{
91
+ text: string;
92
+ toolCalls: ToolCall[];
93
+ usage: Usage;
94
+ cost: number;
95
+ error: string | null;
96
+ msgs: Message[];
97
+ }>;
98
+ /**
99
+ * Health check — validates provider, store, and tools without throwing.
100
+ * @param {ToolDef[]} [tools=[]] - Tool definitions to validate.
101
+ * @returns {Promise<{provider: {ok: boolean, error?: string}, store: {ok: boolean, error?: string, skipped: boolean}, tools: {ok: boolean, errors?: string[]}}>}
102
+ * Never throws — all failures captured in return value.
103
+ */
104
+ validate(tools?: ToolDef[]): Promise<{
105
+ provider: {
106
+ ok: boolean;
107
+ error?: string;
108
+ };
109
+ store: {
110
+ ok: boolean;
111
+ error?: string;
112
+ skipped: boolean;
113
+ };
114
+ tools: {
115
+ ok: boolean;
116
+ errors?: string[];
117
+ };
118
+ }>;
119
+ /**
120
+ * Stateful single-turn chat that maintains conversation history across calls.
121
+ * @param {string} text - User message.
122
+ * @param {ToolDef[]} [tools=[]] - Tool definitions.
123
+ * @param {Record<string, any>} [options={}] - Per-run overrides.
124
+ * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[]}>}
125
+ */
126
+ chat(text: string, tools?: ToolDef[], options?: Record<string, any>): Promise<{
127
+ text: string;
128
+ toolCalls: ToolCall[];
129
+ usage: Usage;
130
+ cost: number;
131
+ error: string | null;
132
+ msgs: Message[];
133
+ }>;
134
+ stop(): void;
135
+ }
package/src/loop.js CHANGED
@@ -2,8 +2,38 @@
2
2
 
3
3
  const { ToolError, HaltError } = require('./errors');
4
4
 
5
+ /** @typedef {import('../types').Provider} Provider */
6
+ /** @typedef {import('../types').Message} Message */
7
+ /** @typedef {import('../types').ToolDef} ToolDef */
8
+ /** @typedef {import('../types').ToolCall} ToolCall */
9
+ /** @typedef {import('../types').Usage} Usage */
10
+ /** @typedef {import('../types').GenerateResult} GenerateResult */
11
+ /** @typedef {import('../types').Store} Store */
12
+ /** @typedef {import('./checkpoint').Checkpoint} Checkpoint */
13
+ /** @typedef {import('./retry').Retry} Retry */
14
+ /** @typedef {import('./stream').Stream} Stream */
15
+
16
+ /**
17
+ * @typedef {object} LoopOptions
18
+ * @property {Provider} provider
19
+ * @property {string} [system]
20
+ * @property {Checkpoint} [checkpoint]
21
+ * @property {Retry} [retry]
22
+ * @property {Stream} [stream]
23
+ * @property {Store} [store]
24
+ * @property {Function} [onToolCall]
25
+ * @property {Function} [onText]
26
+ * @property {Function} [onError]
27
+ * @property {boolean} [throwOnError]
28
+ * @property {Function} [policy]
29
+ * @property {Function} [onLlmResult]
30
+ * @property {Function} [onToolResult]
31
+ * @property {number} [maxRounds] - Removed in v0.8; presence throws a migration error.
32
+ */
33
+
5
34
  // Average pricing per 1K tokens (USD). Adjust these to match your provider's rates.
6
35
  // Last updated: 2026-05-18. Source: public provider pricing pages.
36
+ /** @type {Record<string, {in: number, out: number}>} */
7
37
  const COST_PER_1K = {
8
38
  // OpenAI
9
39
  'gpt-4o': { in: 0.0025, out: 0.01 },
@@ -33,6 +63,10 @@ const HARD_ROUND_LIMIT = 100;
33
63
  // synthetic `role:'tool'` reply for every tool_call_id that has no matching
34
64
  // reply. Halt-path only — keeps msgs a valid OpenAI transcript when the loop
35
65
  // exits between pushing assistant.tool_calls and finishing the per-tool loop.
66
+ /**
67
+ * @param {Message[]} msgs
68
+ * @param {string} rule
69
+ */
36
70
  function sealDanglingToolCalls(msgs, rule) {
37
71
  for (let i = msgs.length - 1; i >= 0; i--) {
38
72
  const m = msgs[i];
@@ -50,6 +84,11 @@ function sealDanglingToolCalls(msgs, rule) {
50
84
  }
51
85
  }
52
86
 
87
+ /**
88
+ * @param {string|null} model
89
+ * @param {Usage|null} usage
90
+ * @returns {number|null}
91
+ */
53
92
  function estimateCost(model, usage) {
54
93
  if (!usage || !model) return null;
55
94
  const rates = COST_PER_1K[model] || COST_PER_1K['_default'];
@@ -61,19 +100,15 @@ function estimateCost(model, usage) {
61
100
 
62
101
  class Loop {
63
102
  /**
64
- * @param {object} options
65
- * @param {object} options.provider - LLM provider (must implement generate()).
66
- * @param {string} [options.system] - System prompt prepended to messages.
67
- * @param {object} [options.checkpoint] - Checkpoint instance for human-in-the-loop.
68
- * @param {object} [options.retry] - Retry instance for backoff on failures.
69
- * @param {object} [options.stream] - Stream instance for event emission.
70
- * @param {object} [options.store] - Store instance for validate() health check.
71
- * @param {Function} [options.policy] - Async (toolName, args, ctx) => true | string. Recommended wiring: closure that delegates to a bareguard Gate (`require('bare-agent/bareguard').wireGate(gate).policy`). Anything other than `true` denies; a string is fed to the LLM verbatim as the deny reason. A throw of `HaltError` exits the loop cleanly. All policy/budget/audit decisions live in bareguard — Loop just calls the closure and respects the verdict.
72
- * @param {Function} [options.onLlmResult] - Async ({model, provider, usage, costUsd, durationMs, ctx}) called after every successful provider.generate. Wire via `wireGate(gate).onLlmResult` so `budget.maxCostUsd` covers token-only workloads. Errors route through `_reportError` but never kill the loop.
73
- * @param {Function} [options.onToolResult] - Async ({name, args, result, error, durationMs, ctx}) called after every tool.execute (success and failure). Wire via `wireGate(gate).onToolResult` so `gate.record` sees `ctx`. Errors route through `_reportError` but never kill the loop.
103
+ * `policy` is async `(toolName, args, ctx) => true | string`. Recommended wiring: a closure
104
+ * that delegates to a bareguard Gate (`require('bare-agent/bareguard').wireGate(gate).policy`).
105
+ * Anything other than `true` denies; a string is fed to the LLM verbatim as the deny reason.
106
+ * A throw of `HaltError` exits the loop cleanly. `onLlmResult`/`onToolResult` forward usage and
107
+ * tool outcomes to `gate.record` (via wireGate) and never kill the loop on error.
108
+ * @param {LoopOptions} options
74
109
  * @throws {Error} `[Loop] requires a provider` — when options.provider is missing.
75
110
  */
76
- constructor(options = {}) {
111
+ constructor(options = /** @type {LoopOptions} */ ({})) {
77
112
  if (!options.provider) throw new Error('[Loop] requires a provider');
78
113
  if (options.maxRounds !== undefined) {
79
114
  throw new Error(
@@ -106,12 +141,18 @@ class Loop {
106
141
  this.onLlmResult = options.onLlmResult || null;
107
142
  this.onToolResult = options.onToolResult || null;
108
143
  this._stopped = false;
144
+ /** @type {Message[]} */
109
145
  this._history = []; // for chat() stateful mode
110
146
  }
111
147
 
112
148
  // Unified error emitter — every silent-ish failure path routes through here so
113
149
  // operators see callback throws, checkpoint timeouts, stream listener errors
114
150
  // in one place: loop:error stream event + onError callback.
151
+ /**
152
+ * @param {string} source
153
+ * @param {any} err
154
+ * @param {Record<string, any>} [extra]
155
+ */
115
156
  _reportError(source, err, extra = {}) {
116
157
  const message = err?.message || String(err);
117
158
  this._safeEmit({ type: 'loop:error', data: { source, error: message, ...extra } });
@@ -125,6 +166,7 @@ class Loop {
125
166
  }
126
167
 
127
168
  // Swallow-proof stream emit: a throwing listener must not corrupt Loop state.
169
+ /** @param {{type: string, data?: any, ts?: string}} event */
128
170
  _safeEmit(event) {
129
171
  if (!this.stream) return;
130
172
  try {
@@ -138,6 +180,11 @@ class Loop {
138
180
  }
139
181
 
140
182
  // Fire a user callback without letting its throw kill the loop.
183
+ /**
184
+ * @param {string} name
185
+ * @param {Function|null} fn
186
+ * @param {...any} args
187
+ */
141
188
  _safeCall(name, fn, ...args) {
142
189
  if (!fn) return;
143
190
  try {
@@ -149,10 +196,10 @@ class Loop {
149
196
 
150
197
  /**
151
198
  * Run the think/act/observe loop.
152
- * @param {Array<object>} messages - Conversation messages in OpenAI format.
153
- * @param {Array<object>} [tools=[]] - Tool definitions with name, execute, description, parameters.
154
- * @param {object} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
155
- * @returns {Promise<{text: string, toolCalls: Array, usage: object, cost: number, error: string|null, msgs: Array<object>}>}
199
+ * @param {Message[]} messages - Conversation messages in OpenAI format.
200
+ * @param {ToolDef[]} [tools=[]] - Tool definitions with name, execute, description, parameters.
201
+ * @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
202
+ * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[]}>}
156
203
  * On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
157
204
  * thrown HaltError carried no `rule`), and `msgs` is sanitized so any
158
205
  * dangling assistant `tool_calls` from the halted round are paired with
@@ -244,7 +291,7 @@ class Loop {
244
291
  msgs.push({
245
292
  role: 'assistant',
246
293
  content: result.text || null,
247
- tool_calls: result.toolCalls.map(tc => ({
294
+ tool_calls: result.toolCalls.map((/** @type {ToolCall} */ tc) => ({
248
295
  id: tc.id,
249
296
  type: 'function',
250
297
  function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
@@ -381,11 +428,12 @@ class Loop {
381
428
 
382
429
  /**
383
430
  * Health check — validates provider, store, and tools without throwing.
384
- * @param {Array<object>} [tools=[]] - Tool definitions to validate.
431
+ * @param {ToolDef[]} [tools=[]] - Tool definitions to validate.
385
432
  * @returns {Promise<{provider: {ok: boolean, error?: string}, store: {ok: boolean, error?: string, skipped: boolean}, tools: {ok: boolean, errors?: string[]}}>}
386
433
  * Never throws — all failures captured in return value.
387
434
  */
388
435
  async validate(tools = []) {
436
+ /** @type {{provider: {ok: boolean, error?: string}, store: {ok: boolean, error?: string, skipped: boolean}, tools: {ok: boolean, errors?: string[]}}} */
389
437
  const result = {
390
438
  provider: { ok: false },
391
439
  store: { ok: false, skipped: false },
@@ -421,6 +469,7 @@ class Loop {
421
469
  }
422
470
 
423
471
  // Tools check
472
+ /** @type {string[]} */
424
473
  const toolErrors = [];
425
474
  for (const tool of tools) {
426
475
  if (typeof tool.name !== 'string' || !tool.name) {
@@ -442,6 +491,13 @@ class Loop {
442
491
  return result;
443
492
  }
444
493
 
494
+ /**
495
+ * Stateful single-turn chat that maintains conversation history across calls.
496
+ * @param {string} text - User message.
497
+ * @param {ToolDef[]} [tools=[]] - Tool definitions.
498
+ * @param {Record<string, any>} [options={}] - Per-run overrides.
499
+ * @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[]}>}
500
+ */
445
501
  async chat(text, tools = [], options = {}) {
446
502
  this._history.push({ role: 'user', content: text });
447
503
  const result = await this.run(this._history, tools, options);