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
package/src/provider-ollama.js
CHANGED
|
@@ -3,7 +3,21 @@
|
|
|
3
3
|
const http = require('http');
|
|
4
4
|
const { ProviderError } = require('./errors');
|
|
5
5
|
|
|
6
|
+
/** @typedef {import('../types').Message} Message */
|
|
7
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
8
|
+
/** @typedef {import('../types').GenerateResult} GenerateResult */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} OllamaOptions
|
|
12
|
+
* @property {string} [model='llama3.2']
|
|
13
|
+
* @property {string} [url='http://localhost:11434']
|
|
14
|
+
* @property {boolean} [exposeErrorBody=false]
|
|
15
|
+
*/
|
|
16
|
+
|
|
6
17
|
class OllamaProvider {
|
|
18
|
+
/**
|
|
19
|
+
* @param {OllamaOptions} [options]
|
|
20
|
+
*/
|
|
7
21
|
constructor(options = {}) {
|
|
8
22
|
this.model = options.model || 'llama3.2';
|
|
9
23
|
this.url = options.url || 'http://localhost:11434';
|
|
@@ -13,13 +27,14 @@ class OllamaProvider {
|
|
|
13
27
|
|
|
14
28
|
/**
|
|
15
29
|
* Generate a response from a local Ollama instance.
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @returns {Promise<
|
|
30
|
+
* @param {Message[]} messages - Conversation messages.
|
|
31
|
+
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
32
|
+
* @param {Record<string, any>} [options={}] - Options (temperature).
|
|
33
|
+
* @returns {Promise<GenerateResult>}
|
|
20
34
|
* @throws {Error} `[OllamaProvider] ...` — on HTTP errors or invalid JSON response.
|
|
21
35
|
*/
|
|
22
36
|
async generate(messages, tools = [], options = {}) {
|
|
37
|
+
/** @type {Record<string, any>} */
|
|
23
38
|
const body = {
|
|
24
39
|
model: this.model,
|
|
25
40
|
messages,
|
|
@@ -38,7 +53,7 @@ class OllamaProvider {
|
|
|
38
53
|
|
|
39
54
|
return {
|
|
40
55
|
text: msg.content || '',
|
|
41
|
-
toolCalls: (msg.tool_calls || []).map(tc => ({
|
|
56
|
+
toolCalls: (msg.tool_calls || []).map((/** @type {any} */ tc) => ({
|
|
42
57
|
id: tc.id || `call_${Date.now()}`,
|
|
43
58
|
name: tc.function.name,
|
|
44
59
|
arguments: typeof tc.function.arguments === 'string'
|
|
@@ -52,6 +67,11 @@ class OllamaProvider {
|
|
|
52
67
|
};
|
|
53
68
|
}
|
|
54
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} path
|
|
72
|
+
* @param {Record<string, any>} body
|
|
73
|
+
* @returns {Promise<any>}
|
|
74
|
+
*/
|
|
55
75
|
_request(path, body) {
|
|
56
76
|
return new Promise((resolve, reject) => {
|
|
57
77
|
const url = new URL(this.url + path);
|
|
@@ -69,10 +89,10 @@ class OllamaProvider {
|
|
|
69
89
|
res.on('end', () => {
|
|
70
90
|
try {
|
|
71
91
|
const parsed = JSON.parse(chunks);
|
|
72
|
-
if (res.statusCode >= 400) {
|
|
92
|
+
if ((res.statusCode ?? 0) >= 400) {
|
|
73
93
|
return reject(new ProviderError(
|
|
74
94
|
`[OllamaProvider] ${parsed.error || `HTTP ${res.statusCode}`}`,
|
|
75
|
-
{ status: res.statusCode, body: this.exposeErrorBody ? parsed : undefined }
|
|
95
|
+
/** @type {any} */ ({ status: res.statusCode, body: this.exposeErrorBody ? parsed : undefined })
|
|
76
96
|
));
|
|
77
97
|
}
|
|
78
98
|
resolve(parsed);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type Message = import("../types").Message;
|
|
2
|
+
export type ToolDef = import("../types").ToolDef;
|
|
3
|
+
export type ToolCall = import("../types").ToolCall;
|
|
4
|
+
export type GenerateResult = import("../types").GenerateResult;
|
|
5
|
+
export type OpenAIOptions = {
|
|
6
|
+
apiKey?: string | undefined;
|
|
7
|
+
model?: string | undefined;
|
|
8
|
+
baseUrl?: string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* - Attach the full upstream
|
|
11
|
+
* response to `err.body` on HTTP errors. Off by default so an unexpected
|
|
12
|
+
* field in an error payload can't leak through logs that dump the error
|
|
13
|
+
* object; `err.message` still carries the API's error message. Turn on for
|
|
14
|
+
* debugging only.
|
|
15
|
+
*/
|
|
16
|
+
exposeErrorBody?: boolean | undefined;
|
|
17
|
+
};
|
|
18
|
+
/** @typedef {import('../types').Message} Message */
|
|
19
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
20
|
+
/** @typedef {import('../types').ToolCall} ToolCall */
|
|
21
|
+
/** @typedef {import('../types').GenerateResult} GenerateResult */
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} OpenAIOptions
|
|
24
|
+
* @property {string} [apiKey]
|
|
25
|
+
* @property {string} [model='gpt-4o-mini']
|
|
26
|
+
* @property {string} [baseUrl='https://api.openai.com/v1']
|
|
27
|
+
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream
|
|
28
|
+
* response to `err.body` on HTTP errors. Off by default so an unexpected
|
|
29
|
+
* field in an error payload can't leak through logs that dump the error
|
|
30
|
+
* object; `err.message` still carries the API's error message. Turn on for
|
|
31
|
+
* debugging only.
|
|
32
|
+
*/
|
|
33
|
+
export class OpenAIProvider {
|
|
34
|
+
/**
|
|
35
|
+
* @param {OpenAIOptions} [options]
|
|
36
|
+
*/
|
|
37
|
+
constructor(options?: OpenAIOptions);
|
|
38
|
+
apiKey: string | undefined;
|
|
39
|
+
model: string;
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
exposeErrorBody: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Generate a response from the OpenAI API.
|
|
44
|
+
* @param {Message[]} messages - Conversation messages.
|
|
45
|
+
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
46
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
|
|
47
|
+
* @returns {Promise<GenerateResult>}
|
|
48
|
+
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
49
|
+
*/
|
|
50
|
+
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} path
|
|
53
|
+
* @param {Record<string, any>} body
|
|
54
|
+
* @returns {Promise<any>}
|
|
55
|
+
*/
|
|
56
|
+
_request(path: string, body: Record<string, any>): Promise<any>;
|
|
57
|
+
}
|
package/src/provider-openai.js
CHANGED
|
@@ -4,17 +4,26 @@ const https = require('https');
|
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const { ProviderError } = require('./errors');
|
|
6
6
|
|
|
7
|
+
/** @typedef {import('../types').Message} Message */
|
|
8
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
9
|
+
/** @typedef {import('../types').ToolCall} ToolCall */
|
|
10
|
+
/** @typedef {import('../types').GenerateResult} GenerateResult */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} OpenAIOptions
|
|
14
|
+
* @property {string} [apiKey]
|
|
15
|
+
* @property {string} [model='gpt-4o-mini']
|
|
16
|
+
* @property {string} [baseUrl='https://api.openai.com/v1']
|
|
17
|
+
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream
|
|
18
|
+
* response to `err.body` on HTTP errors. Off by default so an unexpected
|
|
19
|
+
* field in an error payload can't leak through logs that dump the error
|
|
20
|
+
* object; `err.message` still carries the API's error message. Turn on for
|
|
21
|
+
* debugging only.
|
|
22
|
+
*/
|
|
23
|
+
|
|
7
24
|
class OpenAIProvider {
|
|
8
25
|
/**
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {string} [options.apiKey]
|
|
11
|
-
* @param {string} [options.model='gpt-4o-mini']
|
|
12
|
-
* @param {string} [options.baseUrl='https://api.openai.com/v1']
|
|
13
|
-
* @param {boolean} [options.exposeErrorBody=false] - Attach the full upstream
|
|
14
|
-
* response to `err.body` on HTTP errors. Off by default so an unexpected
|
|
15
|
-
* field in an error payload can't leak through logs that dump the error
|
|
16
|
-
* object; `err.message` still carries the API's error message. Turn on for
|
|
17
|
-
* debugging only.
|
|
26
|
+
* @param {OpenAIOptions} [options]
|
|
18
27
|
*/
|
|
19
28
|
constructor(options = {}) {
|
|
20
29
|
this.apiKey = options.apiKey?.trim();
|
|
@@ -25,13 +34,14 @@ class OpenAIProvider {
|
|
|
25
34
|
|
|
26
35
|
/**
|
|
27
36
|
* Generate a response from the OpenAI API.
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {
|
|
31
|
-
* @returns {Promise<
|
|
37
|
+
* @param {Message[]} messages - Conversation messages.
|
|
38
|
+
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
39
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
|
|
40
|
+
* @returns {Promise<GenerateResult>}
|
|
32
41
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
33
42
|
*/
|
|
34
43
|
async generate(messages, tools = [], options = {}) {
|
|
44
|
+
/** @type {Record<string, any>} */
|
|
35
45
|
const body = {
|
|
36
46
|
model: this.model,
|
|
37
47
|
messages,
|
|
@@ -51,7 +61,7 @@ class OpenAIProvider {
|
|
|
51
61
|
|
|
52
62
|
return {
|
|
53
63
|
text: msg.content || '',
|
|
54
|
-
toolCalls: (msg.tool_calls || []).map(tc => ({
|
|
64
|
+
toolCalls: (msg.tool_calls || []).map((/** @type {any} */ tc) => ({
|
|
55
65
|
id: tc.id,
|
|
56
66
|
name: tc.function.name,
|
|
57
67
|
arguments: JSON.parse(tc.function.arguments),
|
|
@@ -63,6 +73,11 @@ class OpenAIProvider {
|
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} path
|
|
78
|
+
* @param {Record<string, any>} body
|
|
79
|
+
* @returns {Promise<any>}
|
|
80
|
+
*/
|
|
66
81
|
_request(path, body) {
|
|
67
82
|
return new Promise((resolve, reject) => {
|
|
68
83
|
const url = new URL(this.baseUrl + path);
|
|
@@ -82,10 +97,10 @@ class OpenAIProvider {
|
|
|
82
97
|
res.on('end', () => {
|
|
83
98
|
try {
|
|
84
99
|
const parsed = JSON.parse(chunks);
|
|
85
|
-
if (res.statusCode >= 400) {
|
|
100
|
+
if ((res.statusCode ?? 0) >= 400) {
|
|
86
101
|
return reject(new ProviderError(
|
|
87
102
|
`[OpenAIProvider] ${parsed.error?.message || `HTTP ${res.statusCode}`}`,
|
|
88
|
-
{ status: res.statusCode, body: this.exposeErrorBody ? parsed : undefined }
|
|
103
|
+
/** @type {any} */ ({ status: res.statusCode, body: this.exposeErrorBody ? parsed : undefined })
|
|
89
104
|
));
|
|
90
105
|
}
|
|
91
106
|
resolve(parsed);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { OpenAIProvider } from "./provider-openai";
|
|
2
|
+
import { AnthropicProvider } from "./provider-anthropic";
|
|
3
|
+
import { OllamaProvider } from "./provider-ollama";
|
|
4
|
+
import { CLIPipeProvider } from "./provider-clipipe";
|
|
5
|
+
import { FallbackProvider } from "./provider-fallback";
|
|
6
|
+
export { OpenAIProvider as OpenAI, AnthropicProvider as Anthropic, OllamaProvider as Ollama, CLIPipeProvider as CLIPipe, FallbackProvider as Fallback, OpenAIProvider, AnthropicProvider, OllamaProvider, CLIPipeProvider, FallbackProvider };
|
package/src/providers.js
CHANGED
|
@@ -7,9 +7,17 @@ const { CLIPipeProvider } = require('./provider-clipipe');
|
|
|
7
7
|
const { FallbackProvider } = require('./provider-fallback');
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
|
+
// Short names (canonical — used throughout docs and the integration guide)
|
|
10
11
|
OpenAI: OpenAIProvider,
|
|
11
12
|
Anthropic: AnthropicProvider,
|
|
12
13
|
Ollama: OllamaProvider,
|
|
13
14
|
CLIPipe: CLIPipeProvider,
|
|
14
15
|
Fallback: FallbackProvider,
|
|
16
|
+
// *Provider aliases match the class names in source/stack traces, so
|
|
17
|
+
// `const { OpenAIProvider } = require('bare-agent/providers')` also works.
|
|
18
|
+
OpenAIProvider,
|
|
19
|
+
AnthropicProvider,
|
|
20
|
+
OllamaProvider,
|
|
21
|
+
CLIPipeProvider,
|
|
22
|
+
FallbackProvider,
|
|
15
23
|
};
|
package/src/retry.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type RetryOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* - Maximum number of attempts.
|
|
4
|
+
*/
|
|
5
|
+
maxAttempts?: number | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* - Backoff strategy or fixed ms.
|
|
8
|
+
*/
|
|
9
|
+
backoff?: number | "linear" | "exponential" | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* - Per-attempt timeout in ms (0 to disable).
|
|
12
|
+
*/
|
|
13
|
+
timeout?: number | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* - Predicate deciding whether to retry an error.
|
|
16
|
+
*/
|
|
17
|
+
retryOn?: ((err: any) => boolean) | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* - Jitter strategy.
|
|
20
|
+
*/
|
|
21
|
+
jitter?: number | boolean | "full" | "equal" | undefined;
|
|
22
|
+
};
|
|
23
|
+
export class Retry {
|
|
24
|
+
/** @param {RetryOptions} [options={}] */
|
|
25
|
+
constructor(options?: RetryOptions);
|
|
26
|
+
maxAttempts: number;
|
|
27
|
+
backoff: number | "linear" | "exponential";
|
|
28
|
+
timeout: number;
|
|
29
|
+
retryOn: (err: any) => boolean;
|
|
30
|
+
jitter: number | boolean | "full" | "equal";
|
|
31
|
+
/**
|
|
32
|
+
* Call a function with retry logic.
|
|
33
|
+
* @param {() => Promise<*>} fn - Async function to execute.
|
|
34
|
+
* @param {RetryOptions} [options={}] - Per-call overrides for maxAttempts, retryOn, timeout.
|
|
35
|
+
* @returns {Promise<*>} The result of fn().
|
|
36
|
+
* @throws {TimeoutError} When an individual attempt exceeds the timeout.
|
|
37
|
+
* @throws {Error} Rethrows the last error when maxAttempts is exhausted or error is not retryable.
|
|
38
|
+
*/
|
|
39
|
+
call(fn: () => Promise<any>, options?: RetryOptions): Promise<any>;
|
|
40
|
+
/** @param {number} attempt */
|
|
41
|
+
_delay(attempt: number): number;
|
|
42
|
+
/** @param {number} base */
|
|
43
|
+
_applyJitter(base: number): number;
|
|
44
|
+
}
|
package/src/retry.js
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const { TimeoutError } = require('./errors');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} RetryOptions
|
|
7
|
+
* @property {number} [maxAttempts=3] - Maximum number of attempts.
|
|
8
|
+
* @property {number|'linear'|'exponential'} [backoff='exponential'] - Backoff strategy or fixed ms.
|
|
9
|
+
* @property {number} [timeout=60000] - Per-attempt timeout in ms (0 to disable).
|
|
10
|
+
* @property {(err: any) => boolean} [retryOn] - Predicate deciding whether to retry an error.
|
|
11
|
+
* @property {boolean|number|'full'|'equal'} [jitter=false] - Jitter strategy.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** @param {any} err */
|
|
5
15
|
const DEFAULT_RETRY_ON = (err) => {
|
|
6
16
|
if (err.retryable === true) return true;
|
|
7
17
|
if (err.retryable === false) return false;
|
|
@@ -13,6 +23,7 @@ const DEFAULT_RETRY_ON = (err) => {
|
|
|
13
23
|
};
|
|
14
24
|
|
|
15
25
|
class Retry {
|
|
26
|
+
/** @param {RetryOptions} [options={}] */
|
|
16
27
|
constructor(options = {}) {
|
|
17
28
|
this.maxAttempts = options.maxAttempts !== undefined ? options.maxAttempts : 3;
|
|
18
29
|
this.backoff = options.backoff || 'exponential';
|
|
@@ -24,7 +35,7 @@ class Retry {
|
|
|
24
35
|
/**
|
|
25
36
|
* Call a function with retry logic.
|
|
26
37
|
* @param {() => Promise<*>} fn - Async function to execute.
|
|
27
|
-
* @param {
|
|
38
|
+
* @param {RetryOptions} [options={}] - Per-call overrides for maxAttempts, retryOn, timeout.
|
|
28
39
|
* @returns {Promise<*>} The result of fn().
|
|
29
40
|
* @throws {TimeoutError} When an individual attempt exceeds the timeout.
|
|
30
41
|
* @throws {Error} Rethrows the last error when maxAttempts is exhausted or error is not retryable.
|
|
@@ -35,6 +46,7 @@ class Retry {
|
|
|
35
46
|
const timeout = options.timeout !== undefined ? options.timeout : this.timeout;
|
|
36
47
|
|
|
37
48
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
49
|
+
/** @type {NodeJS.Timeout|undefined} */
|
|
38
50
|
let timeoutId;
|
|
39
51
|
try {
|
|
40
52
|
const result = await (timeout
|
|
@@ -56,6 +68,7 @@ class Retry {
|
|
|
56
68
|
}
|
|
57
69
|
}
|
|
58
70
|
|
|
71
|
+
/** @param {number} attempt */
|
|
59
72
|
_delay(attempt) {
|
|
60
73
|
let base;
|
|
61
74
|
if (typeof this.backoff === 'number') {
|
|
@@ -68,6 +81,7 @@ class Retry {
|
|
|
68
81
|
return this._applyJitter(base);
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
/** @param {number} base */
|
|
71
85
|
_applyJitter(base) {
|
|
72
86
|
if (this.jitter === false || this.jitter === 0) return base;
|
|
73
87
|
if (this.jitter === 'full') {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export type StateMachine = import("./state").StateMachine;
|
|
2
|
+
export type Retry = import("./retry").Retry;
|
|
3
|
+
export type Step = {
|
|
4
|
+
/**
|
|
5
|
+
* - Unique step identifier.
|
|
6
|
+
*/
|
|
7
|
+
id: string;
|
|
8
|
+
/**
|
|
9
|
+
* - Description of the step to execute.
|
|
10
|
+
*/
|
|
11
|
+
action: string;
|
|
12
|
+
/**
|
|
13
|
+
* - Ids of steps that must complete first.
|
|
14
|
+
*/
|
|
15
|
+
dependsOn?: string[] | undefined;
|
|
16
|
+
};
|
|
17
|
+
export type TrackingEntry = {
|
|
18
|
+
/**
|
|
19
|
+
* - The (cloned) step being tracked.
|
|
20
|
+
*/
|
|
21
|
+
step: Step;
|
|
22
|
+
/**
|
|
23
|
+
* - Lifecycle status: pending/running/done/failed.
|
|
24
|
+
*/
|
|
25
|
+
status: string;
|
|
26
|
+
/**
|
|
27
|
+
* - Result returned by executeFn on success.
|
|
28
|
+
*/
|
|
29
|
+
result: any;
|
|
30
|
+
/**
|
|
31
|
+
* - Error message on failure.
|
|
32
|
+
*/
|
|
33
|
+
error: string | undefined;
|
|
34
|
+
};
|
|
35
|
+
export type RunPlanOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* - Max parallel steps per wave.
|
|
38
|
+
*/
|
|
39
|
+
concurrency?: number | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* - StateMachine instance for lifecycle tracking.
|
|
42
|
+
*/
|
|
43
|
+
stateMachine?: import("./state").StateMachine | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* - Callback fired when a step begins.
|
|
46
|
+
*/
|
|
47
|
+
onStepStart?: ((step: Step) => void) | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* - Callback fired on success.
|
|
50
|
+
*/
|
|
51
|
+
onStepDone?: ((step: Step, result: any) => void) | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* - Callback fired on failure.
|
|
54
|
+
*/
|
|
55
|
+
onStepFail?: ((step: Step, error: Error) => void) | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* - Callback fired before each wave executes.
|
|
58
|
+
*/
|
|
59
|
+
onWaveStart?: ((waveNumber: number, steps: Step[]) => void) | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* - Optional Retry instance wrapping each step execution.
|
|
62
|
+
*/
|
|
63
|
+
stepRetry?: import("./retry").Retry | undefined;
|
|
64
|
+
};
|
|
65
|
+
export type StepResult = {
|
|
66
|
+
/**
|
|
67
|
+
* - Step id.
|
|
68
|
+
*/
|
|
69
|
+
id: string;
|
|
70
|
+
/**
|
|
71
|
+
* - Final status.
|
|
72
|
+
*/
|
|
73
|
+
status: string;
|
|
74
|
+
/**
|
|
75
|
+
* - Result value if the step succeeded.
|
|
76
|
+
*/
|
|
77
|
+
result?: any;
|
|
78
|
+
/**
|
|
79
|
+
* - Error message if the step failed.
|
|
80
|
+
*/
|
|
81
|
+
error?: string | undefined;
|
|
82
|
+
};
|
|
83
|
+
/** @typedef {import('./state').StateMachine} StateMachine */
|
|
84
|
+
/** @typedef {import('./retry').Retry} Retry */
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {object} Step
|
|
87
|
+
* @property {string} id - Unique step identifier.
|
|
88
|
+
* @property {string} action - Description of the step to execute.
|
|
89
|
+
* @property {string[]} [dependsOn] - Ids of steps that must complete first.
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* @typedef {object} TrackingEntry
|
|
93
|
+
* @property {Step} step - The (cloned) step being tracked.
|
|
94
|
+
* @property {string} status - Lifecycle status: pending/running/done/failed.
|
|
95
|
+
* @property {*} result - Result returned by executeFn on success.
|
|
96
|
+
* @property {string|undefined} error - Error message on failure.
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* @typedef {object} RunPlanOptions
|
|
100
|
+
* @property {number} [concurrency=Infinity] - Max parallel steps per wave.
|
|
101
|
+
* @property {StateMachine} [stateMachine] - StateMachine instance for lifecycle tracking.
|
|
102
|
+
* @property {(step: Step) => void} [onStepStart] - Callback fired when a step begins.
|
|
103
|
+
* @property {(step: Step, result: any) => void} [onStepDone] - Callback fired on success.
|
|
104
|
+
* @property {(step: Step, error: Error) => void} [onStepFail] - Callback fired on failure.
|
|
105
|
+
* @property {(waveNumber: number, steps: Step[]) => void} [onWaveStart] - Callback fired before each wave executes.
|
|
106
|
+
* @property {Retry} [stepRetry] - Optional Retry instance wrapping each step execution.
|
|
107
|
+
*/
|
|
108
|
+
/**
|
|
109
|
+
* @typedef {object} StepResult
|
|
110
|
+
* @property {string} id - Step id.
|
|
111
|
+
* @property {string} status - Final status.
|
|
112
|
+
* @property {*} [result] - Result value if the step succeeded.
|
|
113
|
+
* @property {string} [error] - Error message if the step failed.
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* Execute a step DAG with wave-based parallelism.
|
|
117
|
+
* @param {Step[]} steps - Steps from Planner.
|
|
118
|
+
* @param {(step: Step) => any} executeFn - Async function called for each step: (step) => result.
|
|
119
|
+
* @param {RunPlanOptions} [options={}]
|
|
120
|
+
* @returns {Promise<StepResult[]>}
|
|
121
|
+
* @throws {Error} `[runPlan] steps must be a non-empty array` — when steps is not a non-empty array.
|
|
122
|
+
* @throws {Error} `[runPlan] executeFn must be a function` — when executeFn is not a function.
|
|
123
|
+
* @throws {Error} `[runPlan] duplicate step id: "X"` — when two steps share an id.
|
|
124
|
+
* @throws {Error} `[runPlan] step "X" depends on unknown step "Y"` — when dependsOn references missing id.
|
|
125
|
+
*/
|
|
126
|
+
export function runPlan(steps: Step[], executeFn: (step: Step) => any, options?: RunPlanOptions): Promise<StepResult[]>;
|
package/src/run-plan.js
CHANGED
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/** @typedef {import('./state').StateMachine} StateMachine */
|
|
4
|
+
/** @typedef {import('./retry').Retry} Retry */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} Step
|
|
8
|
+
* @property {string} id - Unique step identifier.
|
|
9
|
+
* @property {string} action - Description of the step to execute.
|
|
10
|
+
* @property {string[]} [dependsOn] - Ids of steps that must complete first.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} TrackingEntry
|
|
15
|
+
* @property {Step} step - The (cloned) step being tracked.
|
|
16
|
+
* @property {string} status - Lifecycle status: pending/running/done/failed.
|
|
17
|
+
* @property {*} result - Result returned by executeFn on success.
|
|
18
|
+
* @property {string|undefined} error - Error message on failure.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} RunPlanOptions
|
|
23
|
+
* @property {number} [concurrency=Infinity] - Max parallel steps per wave.
|
|
24
|
+
* @property {StateMachine} [stateMachine] - StateMachine instance for lifecycle tracking.
|
|
25
|
+
* @property {(step: Step) => void} [onStepStart] - Callback fired when a step begins.
|
|
26
|
+
* @property {(step: Step, result: any) => void} [onStepDone] - Callback fired on success.
|
|
27
|
+
* @property {(step: Step, error: Error) => void} [onStepFail] - Callback fired on failure.
|
|
28
|
+
* @property {(waveNumber: number, steps: Step[]) => void} [onWaveStart] - Callback fired before each wave executes.
|
|
29
|
+
* @property {Retry} [stepRetry] - Optional Retry instance wrapping each step execution.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} StepResult
|
|
34
|
+
* @property {string} id - Step id.
|
|
35
|
+
* @property {string} status - Final status.
|
|
36
|
+
* @property {*} [result] - Result value if the step succeeded.
|
|
37
|
+
* @property {string} [error] - Error message if the step failed.
|
|
38
|
+
*/
|
|
39
|
+
|
|
3
40
|
/**
|
|
4
41
|
* Execute a step DAG with wave-based parallelism.
|
|
5
|
-
* @param {
|
|
6
|
-
* @param {
|
|
7
|
-
* @param {
|
|
8
|
-
* @
|
|
9
|
-
* @param {object} [options.stateMachine] - StateMachine instance for lifecycle tracking.
|
|
10
|
-
* @param {function} [options.onStepStart] - Callback(step) fired when a step begins.
|
|
11
|
-
* @param {function} [options.onStepDone] - Callback(step, result) fired on success.
|
|
12
|
-
* @param {function} [options.onStepFail] - Callback(step, error) fired on failure.
|
|
13
|
-
* @param {function} [options.onWaveStart] - Callback(waveNumber, steps) fired before each wave executes.
|
|
14
|
-
* @returns {Promise<Array<{id: string, status: string, result?: *, error?: string}>>}
|
|
42
|
+
* @param {Step[]} steps - Steps from Planner.
|
|
43
|
+
* @param {(step: Step) => any} executeFn - Async function called for each step: (step) => result.
|
|
44
|
+
* @param {RunPlanOptions} [options={}]
|
|
45
|
+
* @returns {Promise<StepResult[]>}
|
|
15
46
|
* @throws {Error} `[runPlan] steps must be a non-empty array` — when steps is not a non-empty array.
|
|
16
47
|
* @throws {Error} `[runPlan] executeFn must be a function` — when executeFn is not a function.
|
|
17
48
|
* @throws {Error} `[runPlan] duplicate step id: "X"` — when two steps share an id.
|
|
@@ -26,6 +57,7 @@ async function runPlan(steps, executeFn, options = {}) {
|
|
|
26
57
|
}
|
|
27
58
|
|
|
28
59
|
// Build tracking map (don't mutate input)
|
|
60
|
+
/** @type {Map<string, TrackingEntry>} */
|
|
29
61
|
const tracking = new Map();
|
|
30
62
|
for (const step of steps) {
|
|
31
63
|
if (tracking.has(step.id)) {
|
|
@@ -59,7 +91,7 @@ async function runPlan(steps, executeFn, options = {}) {
|
|
|
59
91
|
if (entry.status !== 'pending') continue;
|
|
60
92
|
for (const dep of (entry.step.dependsOn || [])) {
|
|
61
93
|
const depEntry = tracking.get(dep);
|
|
62
|
-
if (depEntry.status === 'failed') {
|
|
94
|
+
if (depEntry && depEntry.status === 'failed') {
|
|
63
95
|
entry.status = 'failed';
|
|
64
96
|
entry.error = `dependency '${dep}' failed`;
|
|
65
97
|
stateMachine?.transition(id, 'start');
|
|
@@ -75,7 +107,7 @@ async function runPlan(steps, executeFn, options = {}) {
|
|
|
75
107
|
for (const [id, entry] of tracking) {
|
|
76
108
|
if (entry.status !== 'pending') continue;
|
|
77
109
|
const deps = entry.step.dependsOn || [];
|
|
78
|
-
const allDone = deps.every(dep => tracking.get(dep)
|
|
110
|
+
const allDone = deps.every(/** @param {string} dep */ dep => tracking.get(dep)?.status === 'done');
|
|
79
111
|
if (allDone) ready.push(entry);
|
|
80
112
|
}
|
|
81
113
|
|
|
@@ -113,7 +145,8 @@ async function runPlan(steps, executeFn, options = {}) {
|
|
|
113
145
|
|
|
114
146
|
// Return results in original order
|
|
115
147
|
return steps.map(s => {
|
|
116
|
-
const entry = tracking.get(s.id);
|
|
148
|
+
const entry = /** @type {TrackingEntry} */ (tracking.get(s.id));
|
|
149
|
+
/** @type {StepResult} */
|
|
117
150
|
const out = { id: s.id, status: entry.status };
|
|
118
151
|
if (entry.result !== undefined) out.result = entry.result;
|
|
119
152
|
if (entry.error !== undefined) out.error = entry.error;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type Job = {
|
|
2
|
+
id: number;
|
|
3
|
+
type: string;
|
|
4
|
+
schedule: string;
|
|
5
|
+
action: any;
|
|
6
|
+
status: string;
|
|
7
|
+
nextRun: string;
|
|
8
|
+
createdAt?: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
export type SchedulerOptions = {
|
|
11
|
+
/**
|
|
12
|
+
* - Path to JSON persistence file.
|
|
13
|
+
*/
|
|
14
|
+
file?: string | null | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* - Tick interval in ms.
|
|
17
|
+
*/
|
|
18
|
+
interval?: number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* - Handler errors callback.
|
|
21
|
+
*/
|
|
22
|
+
onError?: ((err: any, job: Job) => void) | null | undefined;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Time-triggered agent turns. The only way the agent acts without being messaged.
|
|
26
|
+
*
|
|
27
|
+
* Interface:
|
|
28
|
+
* add(job) → jobId
|
|
29
|
+
* remove(jobId) → void
|
|
30
|
+
* list() → [jobs]
|
|
31
|
+
* start(handler) → begin tick loop (handler receives due jobs)
|
|
32
|
+
* stop() → stop tick loop
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} Job
|
|
36
|
+
* @property {number} id
|
|
37
|
+
* @property {string} type
|
|
38
|
+
* @property {string} schedule
|
|
39
|
+
* @property {*} action
|
|
40
|
+
* @property {string} status
|
|
41
|
+
* @property {string} nextRun
|
|
42
|
+
* @property {string} [createdAt]
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} SchedulerOptions
|
|
46
|
+
* @property {string|null} [file] - Path to JSON persistence file.
|
|
47
|
+
* @property {number} [interval=60000] - Tick interval in ms.
|
|
48
|
+
* @property {((err: any, job: Job) => void)|null} [onError] - Handler errors callback.
|
|
49
|
+
*/
|
|
50
|
+
export class Scheduler {
|
|
51
|
+
/** @param {SchedulerOptions} [options={}] */
|
|
52
|
+
constructor(options?: SchedulerOptions);
|
|
53
|
+
_file: string | null;
|
|
54
|
+
_interval: number;
|
|
55
|
+
onError: ((err: any, job: Job) => void) | null;
|
|
56
|
+
/** @type {Job[]} */
|
|
57
|
+
_jobs: Job[];
|
|
58
|
+
/** @type {NodeJS.Timeout|null} */
|
|
59
|
+
_timer: NodeJS.Timeout | null;
|
|
60
|
+
/** @type {Set<number>} */
|
|
61
|
+
_running: Set<number>;
|
|
62
|
+
_nextId: number;
|
|
63
|
+
_save(): void;
|
|
64
|
+
/** @param {{ type?: string, schedule: string, action: * }} job */
|
|
65
|
+
add(job: {
|
|
66
|
+
type?: string;
|
|
67
|
+
schedule: string;
|
|
68
|
+
action: any;
|
|
69
|
+
}): number;
|
|
70
|
+
/** @param {number} jobId */
|
|
71
|
+
remove(jobId: number): void;
|
|
72
|
+
list(): {
|
|
73
|
+
id: number;
|
|
74
|
+
type: string;
|
|
75
|
+
schedule: string;
|
|
76
|
+
action: any;
|
|
77
|
+
status: string;
|
|
78
|
+
nextRun: string;
|
|
79
|
+
createdAt?: string | undefined;
|
|
80
|
+
}[];
|
|
81
|
+
/**
|
|
82
|
+
* Begin the tick loop. Calls `handler(job)` for each due job every tick interval.
|
|
83
|
+
*
|
|
84
|
+
* - `handler` is called with the full job object: `{ id, type, schedule, action, status, nextRun }`.
|
|
85
|
+
* - Jobs that are still running (handler hasn't resolved) are skipped on subsequent ticks
|
|
86
|
+
* via the internal `_running` Set — this prevents overlapping executions of the same job.
|
|
87
|
+
* - Within a single tick, due jobs are executed sequentially (awaited one at a time).
|
|
88
|
+
* - If a handler throws, the error is passed to `onError(err, job)` if configured.
|
|
89
|
+
* The tick loop continues to the next job — handler errors never crash the scheduler.
|
|
90
|
+
*
|
|
91
|
+
* @param {(job: object) => Promise<void>} handler - Async function called for each due job.
|
|
92
|
+
*/
|
|
93
|
+
start(handler: (job: object) => Promise<void>): void;
|
|
94
|
+
stop(): void;
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} schedule - Relative ('5s','30m','2h','1d') or cron expression.
|
|
97
|
+
* @returns {Date} The next run time.
|
|
98
|
+
* @throws {Error} `[Scheduler] Cannot parse schedule` — when format is not recognized.
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
private _parseSchedule;
|
|
102
|
+
}
|