bare-agent 0.10.4 → 0.12.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 (65) hide show
  1. package/bin/cli.d.ts +4 -0
  2. package/bin/cli.js +70 -12
  3. package/bin/test-provider.d.ts +2 -0
  4. package/bin/test-provider.js +5 -1
  5. package/index.d.ts +20 -0
  6. package/package.json +44 -10
  7. package/src/bareguard-adapter.d.ts +118 -0
  8. package/src/bareguard-adapter.js +75 -3
  9. package/src/checkpoint.d.ts +61 -0
  10. package/src/checkpoint.js +17 -8
  11. package/src/circuit-breaker.d.ts +70 -0
  12. package/src/circuit-breaker.js +20 -4
  13. package/src/errors.d.ts +106 -0
  14. package/src/errors.js +50 -1
  15. package/src/loop.d.ts +135 -0
  16. package/src/loop.js +80 -18
  17. package/src/mcp-bridge.d.ts +133 -0
  18. package/src/mcp-bridge.js +199 -26
  19. package/src/mcp.d.ts +4 -0
  20. package/src/memory.d.ts +50 -0
  21. package/src/memory.js +22 -2
  22. package/src/planner.d.ts +62 -0
  23. package/src/planner.js +26 -7
  24. package/src/provider-anthropic.d.ts +55 -0
  25. package/src/provider-anthropic.js +34 -10
  26. package/src/provider-clipipe.d.ts +86 -0
  27. package/src/provider-clipipe.js +28 -18
  28. package/src/provider-fallback.d.ts +44 -0
  29. package/src/provider-fallback.js +18 -8
  30. package/src/provider-ollama.d.ts +41 -0
  31. package/src/provider-ollama.js +29 -7
  32. package/src/provider-openai.d.ts +57 -0
  33. package/src/provider-openai.js +34 -7
  34. package/src/providers.d.ts +6 -0
  35. package/src/retry.d.ts +44 -0
  36. package/src/retry.js +15 -1
  37. package/src/run-plan.d.ts +126 -0
  38. package/src/run-plan.js +46 -13
  39. package/src/scheduler.d.ts +102 -0
  40. package/src/scheduler.js +32 -4
  41. package/src/state.d.ts +45 -0
  42. package/src/state.js +18 -2
  43. package/src/store-jsonfile.d.ts +85 -0
  44. package/src/store-jsonfile.js +50 -8
  45. package/src/store-sqlite.d.ts +90 -0
  46. package/src/store-sqlite.js +31 -7
  47. package/src/stores.d.ts +3 -0
  48. package/src/stream.d.ts +79 -0
  49. package/src/stream.js +32 -0
  50. package/src/tools.d.ts +8 -0
  51. package/src/transport-jsonl.d.ts +30 -0
  52. package/src/transport-jsonl.js +13 -0
  53. package/src/transports.d.ts +2 -0
  54. package/tools/browse.d.ts +10 -0
  55. package/tools/browse.js +2 -0
  56. package/tools/defer.d.ts +33 -0
  57. package/tools/defer.js +12 -3
  58. package/tools/mobile.d.ts +34 -0
  59. package/tools/mobile.js +28 -15
  60. package/tools/shell.d.ts +31 -0
  61. package/tools/shell.js +83 -6
  62. package/tools/spawn.d.ts +107 -0
  63. package/tools/spawn.js +24 -5
  64. package/types/index.d.ts +66 -0
  65. package/types/shims.d.ts +16 -0
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) },
@@ -279,7 +326,13 @@ class Loop {
279
326
  continue;
280
327
  }
281
328
  this._safeEmit({ type: 'checkpoint:reply', data: { reply } });
282
- if (!reply || reply.toLowerCase() === 'no' || reply.toLowerCase() === 'n') {
329
+ // Fail-closed: approve ONLY on an explicit affirmative. Any other reply —
330
+ // an unrecognized string ("denied", "wait"), empty, or a non-string — denies.
331
+ // A human approval gate must never approve on ambiguous input, and reading
332
+ // .toLowerCase() off a non-string here used to throw out of run().
333
+ const approved = typeof reply === 'string'
334
+ && ['yes', 'y', 'approve', 'approved'].includes(reply.trim().toLowerCase());
335
+ if (!approved) {
283
336
  msgs.push({ role: 'tool', tool_call_id: tc.id, content: 'User denied this action.' });
284
337
  continue;
285
338
  }
@@ -375,11 +428,12 @@ class Loop {
375
428
 
376
429
  /**
377
430
  * Health check — validates provider, store, and tools without throwing.
378
- * @param {Array<object>} [tools=[]] - Tool definitions to validate.
431
+ * @param {ToolDef[]} [tools=[]] - Tool definitions to validate.
379
432
  * @returns {Promise<{provider: {ok: boolean, error?: string}, store: {ok: boolean, error?: string, skipped: boolean}, tools: {ok: boolean, errors?: string[]}}>}
380
433
  * Never throws — all failures captured in return value.
381
434
  */
382
435
  async validate(tools = []) {
436
+ /** @type {{provider: {ok: boolean, error?: string}, store: {ok: boolean, error?: string, skipped: boolean}, tools: {ok: boolean, errors?: string[]}}} */
383
437
  const result = {
384
438
  provider: { ok: false },
385
439
  store: { ok: false, skipped: false },
@@ -415,6 +469,7 @@ class Loop {
415
469
  }
416
470
 
417
471
  // Tools check
472
+ /** @type {string[]} */
418
473
  const toolErrors = [];
419
474
  for (const tool of tools) {
420
475
  if (typeof tool.name !== 'string' || !tool.name) {
@@ -436,6 +491,13 @@ class Loop {
436
491
  return result;
437
492
  }
438
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
+ */
439
501
  async chat(text, tools = [], options = {}) {
440
502
  this._history.push({ role: 'user', content: text });
441
503
  const result = await this.run(this._history, tools, options);
@@ -0,0 +1,133 @@
1
+ export type ToolDef = import("../types").ToolDef;
2
+ /**
3
+ * A server definition as found in an IDE/MCP config file.
4
+ */
5
+ export type ServerDef = {
6
+ command: string;
7
+ args?: string[] | undefined;
8
+ env?: Record<string, string> | undefined;
9
+ cwd?: string | undefined;
10
+ };
11
+ /**
12
+ * Raw tool descriptor as returned by an MCP server's tools/list.
13
+ */
14
+ export type McpTool = {
15
+ name: string;
16
+ description?: string | undefined;
17
+ inputSchema?: Record<string, any> | undefined;
18
+ };
19
+ /**
20
+ * Per-server entry persisted in .mcp-bridge.json.
21
+ */
22
+ export type BridgeServerEntry = {
23
+ command: string;
24
+ args: string[];
25
+ env?: Record<string, string> | undefined;
26
+ cwd?: string | undefined;
27
+ /**
28
+ * - tool name -> "allow" | "deny"
29
+ */
30
+ tools: Record<string, string>;
31
+ };
32
+ /**
33
+ * Persisted bridge config (.mcp-bridge.json).
34
+ */
35
+ export type BridgeConfig = {
36
+ /**
37
+ * - ISO timestamp
38
+ */
39
+ discovered: string;
40
+ ttl: string;
41
+ servers: Record<string, BridgeServerEntry>;
42
+ };
43
+ /**
44
+ * A denied-tool descriptor surfaced to the LLM.
45
+ */
46
+ export type DeniedTool = {
47
+ server: string;
48
+ tool: string;
49
+ description: string;
50
+ };
51
+ /**
52
+ * JSON-RPC stdio client over a spawned MCP server.
53
+ */
54
+ export type RpcClient = {
55
+ rpc: (method: string, params?: object) => Promise<any>;
56
+ notify: (method: string, params?: object) => void;
57
+ child: import("node:child_process").ChildProcessWithoutNullStreams;
58
+ stderr: string;
59
+ };
60
+ /**
61
+ * Create an MCP bridge. On first run, discovers MCP servers from IDE configs,
62
+ * connects, lists tools, and writes .mcp-bridge.json with all tools set to "allow".
63
+ * On subsequent runs, reads .mcp-bridge.json and respects allow/deny per tool.
64
+ * Re-discovers when TTL expires (default: 24h).
65
+ *
66
+ * Returns BOTH surfaces (v0.9+):
67
+ * - `tools` — bulk-loaded array of name-prefixed tools (small catalogs;
68
+ * LLM sees them upfront).
69
+ * - `metaTools` — [mcp_discover, mcp_invoke] LLM-callable pair (large catalogs;
70
+ * LLM picks tools dynamically). Shares the same RPC connections.
71
+ *
72
+ * Wire one or the other into Loop's tool array; never both (the LLM would see
73
+ * the same MCP tool twice). Pick by catalog size and token budget.
74
+ *
75
+ * @param {object} [opts]
76
+ * @param {string} [opts.bridgePath] - Path to .mcp-bridge.json. Default: .mcp-bridge.json in cwd.
77
+ * @param {string[]} [opts.configPaths] - IDE config paths for discovery.
78
+ * @param {string[]} [opts.servers] - Limit to these server names.
79
+ * @param {number} [opts.timeout=15000] - Per-server init timeout in ms.
80
+ * @param {boolean} [opts.refresh=false] - Force re-discovery regardless of TTL.
81
+ * @param {(name: string, def: ServerDef) => boolean | Promise<boolean>} [opts.confirmServer]
82
+ * Vet each discovered server BEFORE its `command` is spawned. Connecting to an
83
+ * MCP server runs its command, and discovery reads configs from the cwd (a
84
+ * `.mcp.json` in an untrusted repo) as well as the user's home/IDE configs.
85
+ * Return false to skip a server (its command is never executed). A throw is
86
+ * treated as a deny (fail-closed). Default: every discovered server is trusted
87
+ * (unchanged behavior) — pass this to gate command execution.
88
+ * @returns {Promise<{tools: ToolDef[], metaTools?: ToolDef[], servers: string[], systemContext: string, denied: DeniedTool[], errors?: Array<{server: string, error: string}>, close: Function}>}
89
+ */
90
+ export function createMCPBridge(opts?: {
91
+ bridgePath?: string | undefined;
92
+ configPaths?: string[] | undefined;
93
+ servers?: string[] | undefined;
94
+ timeout?: number | undefined;
95
+ refresh?: boolean | undefined;
96
+ confirmServer?: ((name: string, def: ServerDef) => boolean | Promise<boolean>) | undefined;
97
+ }): Promise<{
98
+ tools: ToolDef[];
99
+ metaTools?: ToolDef[];
100
+ servers: string[];
101
+ systemContext: string;
102
+ denied: DeniedTool[];
103
+ errors?: Array<{
104
+ server: string;
105
+ error: string;
106
+ }>;
107
+ close: Function;
108
+ }>;
109
+ /**
110
+ * @param {string[]} [configPaths]
111
+ * @returns {Map<string, ServerDef>}
112
+ */
113
+ export function discoverServers(configPaths?: string[]): Map<string, ServerDef>;
114
+ /**
115
+ * Build the LLM-callable meta-tool surface from a fully-connected bridge.
116
+ * Shares the underlying tool array and RPC clients with the bulk surface —
117
+ * one set of connections, one factory, two output forms. The user picks
118
+ * `bridge.tools` (bulk) for small catalogs the LLM should see upfront, or
119
+ * `bridge.metaTools` for large catalogs the LLM should discover on demand.
120
+ *
121
+ * Gov shape: when the LLM calls mcp_invoke, the action sent to gate.check
122
+ * is `{ type: 'mcp_invoke', args: { name, args }, _ctx }` — bareguard sees
123
+ * `mcp_invoke` as the type. To deny specific MCP tools, use bareguard's
124
+ * `tools.denyArgPatterns: { mcp_invoke: [/"name":"linear_admin_.*"/] }`
125
+ * or `content.denyPatterns` over the JSON-serialized form. The inner MCP
126
+ * tool name doesn't travel as `action.type` — that's a deliberate v0.9
127
+ * trade for one consistent gate-check call per LLM tool invocation.
128
+ *
129
+ * @param {ToolDef[]} tools - The bulk-loaded, name-prefixed tools array.
130
+ * @param {string} [discoveredAt] - ISO timestamp from .mcp-bridge.json.
131
+ * @returns {ToolDef[]} [mcp_discover, mcp_invoke]
132
+ */
133
+ export function buildMetaTools(tools: ToolDef[], discoveredAt?: string): ToolDef[];