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.
- package/README.md +1 -0
- package/bareagent.context.md +1149 -0
- package/bin/cli.d.ts +4 -0
- package/bin/cli.js +40 -10
- package/bin/test-provider.d.ts +2 -0
- package/bin/test-provider.js +5 -1
- package/index.d.ts +20 -0
- package/package.json +46 -10
- package/src/bareguard-adapter.d.ts +118 -0
- package/src/bareguard-adapter.js +75 -3
- package/src/checkpoint.d.ts +61 -0
- package/src/checkpoint.js +17 -8
- package/src/circuit-breaker.d.ts +70 -0
- package/src/circuit-breaker.js +20 -4
- package/src/errors.d.ts +106 -0
- package/src/errors.js +50 -1
- package/src/loop.d.ts +135 -0
- package/src/loop.js +73 -17
- package/src/mcp-bridge.d.ts +133 -0
- package/src/mcp-bridge.js +179 -27
- package/src/mcp.d.ts +4 -0
- package/src/memory.d.ts +50 -0
- package/src/memory.js +22 -2
- package/src/planner.d.ts +62 -0
- package/src/planner.js +26 -7
- package/src/provider-anthropic.d.ts +55 -0
- package/src/provider-anthropic.js +32 -11
- package/src/provider-clipipe.d.ts +86 -0
- package/src/provider-clipipe.js +28 -18
- package/src/provider-fallback.d.ts +44 -0
- package/src/provider-fallback.js +18 -8
- package/src/provider-ollama.d.ts +41 -0
- package/src/provider-ollama.js +27 -7
- package/src/provider-openai.d.ts +57 -0
- package/src/provider-openai.js +31 -16
- package/src/providers.d.ts +6 -0
- package/src/providers.js +8 -0
- package/src/retry.d.ts +44 -0
- package/src/retry.js +15 -1
- package/src/run-plan.d.ts +126 -0
- package/src/run-plan.js +46 -13
- package/src/scheduler.d.ts +102 -0
- package/src/scheduler.js +32 -4
- package/src/state.d.ts +45 -0
- package/src/state.js +18 -2
- package/src/store-jsonfile.d.ts +85 -0
- package/src/store-jsonfile.js +33 -8
- package/src/store-sqlite.d.ts +90 -0
- package/src/store-sqlite.js +31 -7
- package/src/stores.d.ts +3 -0
- package/src/stream.d.ts +79 -0
- package/src/stream.js +32 -0
- package/src/tools.d.ts +8 -0
- package/src/transport-jsonl.d.ts +30 -0
- package/src/transport-jsonl.js +13 -0
- package/src/transports.d.ts +2 -0
- package/tools/browse.d.ts +10 -0
- package/tools/browse.js +2 -0
- package/tools/defer.d.ts +33 -0
- package/tools/defer.js +12 -3
- package/tools/mobile.d.ts +34 -0
- package/tools/mobile.js +28 -15
- package/tools/shell.d.ts +31 -0
- package/tools/shell.js +55 -6
- package/tools/spawn.d.ts +107 -0
- package/tools/spawn.js +24 -5
- package/types/index.d.ts +66 -0
- 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
|
+
}
|
package/src/circuit-breaker.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
116
|
+
* @param {{ generate: (...args: any[]) => Promise<any> }} provider - Provider with generate().
|
|
102
117
|
* @param {string} [key] - Circuit key.
|
|
103
|
-
* @returns {
|
|
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
|
}
|
package/src/errors.d.ts
ADDED
|
@@ -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
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* @param {
|
|
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 {
|
|
153
|
-
* @param {
|
|
154
|
-
* @param {
|
|
155
|
-
* @returns {Promise<{text: string, toolCalls:
|
|
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 {
|
|
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);
|