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
@@ -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 {Array<object>} messages - Conversation messages.
17
- * @param {Array<object>} [tools=[]] - Tool definitions.
18
- * @param {object} [options={}] - Options (temperature).
19
- * @returns {Promise<{text: string, toolCalls: Array, usage: object}>}
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
+ }
@@ -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 {object} [options]
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 {Array<object>} messages - Conversation messages.
29
- * @param {Array<object>} [tools=[]] - Tool definitions.
30
- * @param {object} [options={}] - Options (temperature, maxTokens).
31
- * @returns {Promise<{text: string, toolCalls: Array, usage: object}>}
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 {object} [options={}] - Per-call overrides for maxAttempts, retryOn, timeout.
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 {Array<{id: string, action: string, dependsOn?: string[]}>} steps - Steps from Planner.
6
- * @param {function} executeFn - Async function called for each step: (step) => result.
7
- * @param {object} [options={}]
8
- * @param {number} [options.concurrency=Infinity] - Max parallel steps per wave.
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).status === 'done');
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
+ }