bare-agent 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/loop.js +65 -0
- package/src/provider-clipipe.js +113 -0
- package/src/providers.js +2 -0
- package/src/run-plan.js +116 -0
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const { Checkpoint } = require('./src/checkpoint');
|
|
|
8
8
|
const { Memory } = require('./src/memory');
|
|
9
9
|
const { Stream } = require('./src/stream');
|
|
10
10
|
const { Retry } = require('./src/retry');
|
|
11
|
+
const { runPlan } = require('./src/run-plan');
|
|
11
12
|
|
|
12
13
|
module.exports = {
|
|
13
14
|
Loop,
|
|
@@ -18,4 +19,5 @@ module.exports = {
|
|
|
18
19
|
Memory,
|
|
19
20
|
Stream,
|
|
20
21
|
Retry,
|
|
22
|
+
runPlan,
|
|
21
23
|
};
|
package/package.json
CHANGED
package/src/loop.js
CHANGED
|
@@ -9,6 +9,7 @@ class Loop {
|
|
|
9
9
|
* @param {object} [options.checkpoint] - Checkpoint instance for human-in-the-loop.
|
|
10
10
|
* @param {object} [options.retry] - Retry instance for backoff on failures.
|
|
11
11
|
* @param {object} [options.stream] - Stream instance for event emission.
|
|
12
|
+
* @param {object} [options.store] - Store instance for validate() health check.
|
|
12
13
|
* @throws {Error} `[Loop] requires a provider` — when options.provider is missing.
|
|
13
14
|
*/
|
|
14
15
|
constructor(options = {}) {
|
|
@@ -22,6 +23,7 @@ class Loop {
|
|
|
22
23
|
this.onToolCall = options.onToolCall || null;
|
|
23
24
|
this.onText = options.onText || null;
|
|
24
25
|
this.onError = options.onError || null;
|
|
26
|
+
this.store = options.store || null;
|
|
25
27
|
this._stopped = false;
|
|
26
28
|
this._history = []; // for chat() stateful mode
|
|
27
29
|
}
|
|
@@ -146,6 +148,69 @@ class Loop {
|
|
|
146
148
|
return { text: '', toolCalls: [], usage: lastUsage, error: warning };
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Health check — validates provider, store, and tools without throwing.
|
|
153
|
+
* @param {Array<object>} [tools=[]] - Tool definitions to validate.
|
|
154
|
+
* @returns {Promise<{provider: {ok: boolean, error?: string}, store: {ok: boolean, error?: string, skipped: boolean}, tools: {ok: boolean, errors?: string[]}}>}
|
|
155
|
+
* Never throws — all failures captured in return value.
|
|
156
|
+
*/
|
|
157
|
+
async validate(tools = []) {
|
|
158
|
+
const result = {
|
|
159
|
+
provider: { ok: false },
|
|
160
|
+
store: { ok: false, skipped: false },
|
|
161
|
+
tools: { ok: true },
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Provider check
|
|
165
|
+
try {
|
|
166
|
+
await this.provider.generate([{ role: 'user', content: 'respond with ok' }], [], {});
|
|
167
|
+
result.provider.ok = true;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
result.provider.error = err.message;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Store check
|
|
173
|
+
if (!this.store) {
|
|
174
|
+
result.store.ok = true;
|
|
175
|
+
result.store.skipped = true;
|
|
176
|
+
} else {
|
|
177
|
+
try {
|
|
178
|
+
const testKey = `__validate_${Date.now()}`;
|
|
179
|
+
await this.store.store(testKey, { test: true });
|
|
180
|
+
const got = await this.store.get(testKey);
|
|
181
|
+
if (got === null || got === undefined) {
|
|
182
|
+
result.store.error = 'store.get returned null for test key';
|
|
183
|
+
} else {
|
|
184
|
+
await this.store.delete(testKey);
|
|
185
|
+
result.store.ok = true;
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
result.store.error = err.message;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Tools check
|
|
193
|
+
const toolErrors = [];
|
|
194
|
+
for (const tool of tools) {
|
|
195
|
+
if (typeof tool.name !== 'string' || !tool.name) {
|
|
196
|
+
toolErrors.push(`Tool is missing a name (got ${JSON.stringify(tool.name)})`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (typeof tool.execute !== 'function') {
|
|
200
|
+
toolErrors.push(`Tool "${tool.name}" is missing an execute() function`);
|
|
201
|
+
}
|
|
202
|
+
if (tool.parameters !== undefined && (typeof tool.parameters !== 'object' || tool.parameters === null)) {
|
|
203
|
+
toolErrors.push(`Tool "${tool.name}" has invalid parameters — expected an object, got ${typeof tool.parameters}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (toolErrors.length > 0) {
|
|
207
|
+
result.tools.ok = false;
|
|
208
|
+
result.tools.errors = toolErrors;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
149
214
|
async chat(text, tools = [], options = {}) {
|
|
150
215
|
this._history.push({ role: 'user', content: text });
|
|
151
216
|
const result = await this.run(this._history, tools, options);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
class CLIPipeProvider {
|
|
6
|
+
/**
|
|
7
|
+
* Provider that pipes prompts to a CLI command via stdin and reads stdout.
|
|
8
|
+
* @param {object} options
|
|
9
|
+
* @param {string} options.command - CLI command to spawn (required).
|
|
10
|
+
* @param {string[]} [options.args=[]] - Arguments to pass to the command.
|
|
11
|
+
* @param {string} [options.cwd] - Working directory for the child process.
|
|
12
|
+
* @param {object} [options.env] - Environment variables for the child process.
|
|
13
|
+
* @param {number} [options.timeout=30000] - Timeout in milliseconds.
|
|
14
|
+
* @throws {Error} `[CLIPipeProvider] requires command` — when options.command is missing.
|
|
15
|
+
*/
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
if (!options.command) throw new Error('[CLIPipeProvider] requires command');
|
|
18
|
+
this.command = options.command;
|
|
19
|
+
this.args = options.args || [];
|
|
20
|
+
this.cwd = options.cwd || undefined;
|
|
21
|
+
this.env = options.env || undefined;
|
|
22
|
+
this.timeout = options.timeout ?? 30000;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a response by piping messages to the CLI command.
|
|
27
|
+
* @param {Array<object>} messages - Conversation messages in OpenAI format.
|
|
28
|
+
* @param {Array<object>} [tools=[]] - Unused (CLI commands don't support tools).
|
|
29
|
+
* @param {object} [options={}] - Unused.
|
|
30
|
+
* @returns {Promise<{text: string, toolCalls: Array, usage: object}>}
|
|
31
|
+
* @throws {Error} `[CLIPipeProvider] failed to spawn "cmd": ...` — when the command cannot be found or executed.
|
|
32
|
+
* @throws {Error} `[CLIPipeProvider] process exited with code N: ...` — on non-zero exit.
|
|
33
|
+
* @throws {Error} `[CLIPipeProvider] timed out after Nms` — when the process exceeds timeout.
|
|
34
|
+
* @throws {Error} `[CLIPipeProvider] process produced no output` — when stdout is empty.
|
|
35
|
+
*/
|
|
36
|
+
async generate(messages, tools = [], options = {}) {
|
|
37
|
+
const prompt = this._formatPrompt(messages);
|
|
38
|
+
const text = await this._spawn(prompt);
|
|
39
|
+
return {
|
|
40
|
+
text,
|
|
41
|
+
toolCalls: [],
|
|
42
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert OpenAI-format messages to a plain text prompt.
|
|
48
|
+
* @param {Array<object>} messages
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
_formatPrompt(messages) {
|
|
52
|
+
return messages.map(m => {
|
|
53
|
+
const role = m.role.charAt(0).toUpperCase() + m.role.slice(1);
|
|
54
|
+
return `${role}: ${m.content}`;
|
|
55
|
+
}).join('\n') + '\n';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Spawn the CLI process, pipe prompt to stdin, collect stdout.
|
|
60
|
+
* @param {string} prompt
|
|
61
|
+
* @returns {Promise<string>}
|
|
62
|
+
*/
|
|
63
|
+
_spawn(prompt) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const child = spawn(this.command, this.args, {
|
|
66
|
+
cwd: this.cwd,
|
|
67
|
+
env: this.env,
|
|
68
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let stdout = '';
|
|
72
|
+
let stderr = '';
|
|
73
|
+
let killed = false;
|
|
74
|
+
|
|
75
|
+
child.stdout.on('data', d => { stdout += d; });
|
|
76
|
+
child.stderr.on('data', d => { stderr += d; });
|
|
77
|
+
|
|
78
|
+
child.on('error', err => {
|
|
79
|
+
reject(new Error(`[CLIPipeProvider] failed to spawn "${this.command}": ${err.message}`));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
child.on('close', code => {
|
|
83
|
+
if (killed) return; // timeout already rejected
|
|
84
|
+
if (code !== 0) {
|
|
85
|
+
return reject(new Error(`[CLIPipeProvider] process exited with code ${code}: ${stderr.trim()}`));
|
|
86
|
+
}
|
|
87
|
+
const text = stdout.trim();
|
|
88
|
+
if (!text) {
|
|
89
|
+
return reject(new Error('[CLIPipeProvider] process produced no output'));
|
|
90
|
+
}
|
|
91
|
+
resolve(text);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Timeout handling
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
killed = true;
|
|
97
|
+
child.kill('SIGTERM');
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
try { child.kill('SIGKILL'); } catch (_) {}
|
|
100
|
+
}, 1000);
|
|
101
|
+
reject(new Error(`[CLIPipeProvider] timed out after ${this.timeout}ms`));
|
|
102
|
+
}, this.timeout);
|
|
103
|
+
|
|
104
|
+
child.on('close', () => clearTimeout(timer));
|
|
105
|
+
|
|
106
|
+
// Write prompt to stdin — catch errors silently (process may exit early)
|
|
107
|
+
child.stdin.on('error', () => {});
|
|
108
|
+
child.stdin.end(prompt);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { CLIPipeProvider };
|
package/src/providers.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
const { OpenAIProvider } = require('./provider-openai');
|
|
4
4
|
const { AnthropicProvider } = require('./provider-anthropic');
|
|
5
5
|
const { OllamaProvider } = require('./provider-ollama');
|
|
6
|
+
const { CLIPipeProvider } = require('./provider-clipipe');
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
OpenAI: OpenAIProvider,
|
|
9
10
|
Anthropic: AnthropicProvider,
|
|
10
11
|
Ollama: OllamaProvider,
|
|
12
|
+
CLIPipe: CLIPipeProvider,
|
|
11
13
|
};
|
package/src/run-plan.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 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
|
+
* @returns {Promise<Array<{id: string, status: string, result?: *, error?: string}>>}
|
|
14
|
+
* @throws {Error} `[runPlan] steps must be a non-empty array` — when steps is not a non-empty array.
|
|
15
|
+
* @throws {Error} `[runPlan] executeFn must be a function` — when executeFn is not a function.
|
|
16
|
+
* @throws {Error} `[runPlan] duplicate step id: "X"` — when two steps share an id.
|
|
17
|
+
* @throws {Error} `[runPlan] step "X" depends on unknown step "Y"` — when dependsOn references missing id.
|
|
18
|
+
*/
|
|
19
|
+
async function runPlan(steps, executeFn, options = {}) {
|
|
20
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
21
|
+
throw new Error('[runPlan] steps must be a non-empty array');
|
|
22
|
+
}
|
|
23
|
+
if (typeof executeFn !== 'function') {
|
|
24
|
+
throw new Error('[runPlan] executeFn must be a function');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Build tracking map (don't mutate input)
|
|
28
|
+
const tracking = new Map();
|
|
29
|
+
for (const step of steps) {
|
|
30
|
+
if (tracking.has(step.id)) {
|
|
31
|
+
throw new Error(`[runPlan] duplicate step id: "${step.id}"`);
|
|
32
|
+
}
|
|
33
|
+
tracking.set(step.id, {
|
|
34
|
+
step: { ...step },
|
|
35
|
+
status: 'pending',
|
|
36
|
+
result: undefined,
|
|
37
|
+
error: undefined,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate dependencies
|
|
42
|
+
for (const step of steps) {
|
|
43
|
+
for (const dep of (step.dependsOn || [])) {
|
|
44
|
+
if (!tracking.has(dep)) {
|
|
45
|
+
throw new Error(`[runPlan] step "${step.id}" depends on unknown step "${dep}"`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { concurrency = Infinity, stateMachine, onStepStart, onStepDone, onStepFail } = options;
|
|
51
|
+
|
|
52
|
+
// Wave loop
|
|
53
|
+
while (true) {
|
|
54
|
+
// Propagate dependency failures
|
|
55
|
+
for (const [id, entry] of tracking) {
|
|
56
|
+
if (entry.status !== 'pending') continue;
|
|
57
|
+
for (const dep of (entry.step.dependsOn || [])) {
|
|
58
|
+
const depEntry = tracking.get(dep);
|
|
59
|
+
if (depEntry.status === 'failed') {
|
|
60
|
+
entry.status = 'failed';
|
|
61
|
+
entry.error = `dependency '${dep}' failed`;
|
|
62
|
+
stateMachine?.transition(id, 'start');
|
|
63
|
+
stateMachine?.transition(id, 'fail', entry.error);
|
|
64
|
+
onStepFail?.(entry.step, new Error(entry.error));
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Find ready steps: pending + all deps done
|
|
71
|
+
const ready = [];
|
|
72
|
+
for (const [id, entry] of tracking) {
|
|
73
|
+
if (entry.status !== 'pending') continue;
|
|
74
|
+
const deps = entry.step.dependsOn || [];
|
|
75
|
+
const allDone = deps.every(dep => tracking.get(dep).status === 'done');
|
|
76
|
+
if (allDone) ready.push(entry);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (ready.length === 0) break;
|
|
80
|
+
|
|
81
|
+
// Apply concurrency limit
|
|
82
|
+
const wave = ready.slice(0, concurrency);
|
|
83
|
+
|
|
84
|
+
// Execute wave
|
|
85
|
+
await Promise.all(wave.map(async entry => {
|
|
86
|
+
const { step } = entry;
|
|
87
|
+
entry.status = 'running';
|
|
88
|
+
stateMachine?.transition(step.id, 'start');
|
|
89
|
+
onStepStart?.(step);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const result = await executeFn(step);
|
|
93
|
+
entry.status = 'done';
|
|
94
|
+
entry.result = result;
|
|
95
|
+
stateMachine?.transition(step.id, 'complete', result);
|
|
96
|
+
onStepDone?.(step, result);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
entry.status = 'failed';
|
|
99
|
+
entry.error = err.message;
|
|
100
|
+
stateMachine?.transition(step.id, 'fail', err.message);
|
|
101
|
+
onStepFail?.(step, err);
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Return results in original order
|
|
107
|
+
return steps.map(s => {
|
|
108
|
+
const entry = tracking.get(s.id);
|
|
109
|
+
const out = { id: s.id, status: entry.status };
|
|
110
|
+
if (entry.result !== undefined) out.result = entry.result;
|
|
111
|
+
if (entry.error !== undefined) out.error = entry.error;
|
|
112
|
+
return out;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { runPlan };
|