bare-agent 0.12.1 → 0.12.2
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/bareagent.context.md +1 -1
- package/examples/README.md +14 -0
- package/examples/mcp-bridge-concurrent.js +106 -0
- package/examples/mcp-bridge-poc.js +77 -0
- package/examples/orchestrator/README.md +53 -0
- package/examples/orchestrator/orchestrator.json +14 -0
- package/examples/orchestrator/specialists/researcher.json +12 -0
- package/examples/orchestrator/specialists/summarizer.json +11 -0
- package/examples/replay-job.js +213 -0
- package/examples/wake.md +99 -0
- package/examples/wake.sh +84 -0
- package/examples/with-bareguard.mjs +65 -0
- package/package.json +2 -1
package/bareagent.context.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bareagent — Integration Guide
|
|
2
2
|
|
|
3
3
|
> For AI assistants and developers wiring bareagent into a project.
|
|
4
|
-
> v0.12.
|
|
4
|
+
> v0.12.2 | Node.js >= 18 | one required dep (`bareguard ^0.4.2`) | Apache 2.0
|
|
5
5
|
>
|
|
6
6
|
> Full human guide with composition examples, design philosophy, and recipes: [Usage Guide](docs/02-features/usage-guide.md)
|
|
7
7
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# examples
|
|
2
|
+
|
|
3
|
+
Runnable reference scripts for bare-agent. Each is self-contained — the top-of-file docstring documents flags and required env vars. These ship in the npm package, so you can copy them straight out of `node_modules/bare-agent/examples/`.
|
|
4
|
+
|
|
5
|
+
| Example | What it shows |
|
|
6
|
+
|---------|---------------|
|
|
7
|
+
| [`with-bareguard.mjs`](with-bareguard.mjs) | End-to-end Loop + bareguard wiring: budget cap, fs scope, bash allowlist, audit log, humanChannel. The canonical governed-loop reference. |
|
|
8
|
+
| [`mcp-bridge-poc.js`](mcp-bridge-poc.js) | Auto-discover MCP servers from your IDE configs and expose them as bareagent tools. First run writes `.mcp-bridge.json` (edit to deny tools). |
|
|
9
|
+
| [`mcp-bridge-concurrent.js`](mcp-bridge-concurrent.js) | Soak test: fan out concurrent `barebrowse_browse` calls against real domains (Amazon, Wikipedia, GitHub, a dead host) and verify resilience. |
|
|
10
|
+
| [`orchestrator/`](orchestrator/) | Multi-agent dispatch via `spawn`. Three configs, one system prompt — no orchestrator class, no role types. Roles are JSON files. See its [README](orchestrator/README.md). |
|
|
11
|
+
| [`wake.sh`](wake.sh) + [`wake.md`](wake.md) | Reference cron + jq script for firing deferred actions. The runtime half of `createDeferTool` — bareagent emits, `wake.sh` fires. |
|
|
12
|
+
| [`replay-job.js`](replay-job.js) | Supervised replay POC: record a browser task once with the LLM driving, then replay against fresh snapshots with the LLM as locator-only. Falls back to full reasoning when the locator misses, and patches the trace. |
|
|
13
|
+
|
|
14
|
+
For wiring recipes and API details see the [Integration Guide](../bareagent.context.md); for usage patterns and design philosophy see the [Usage Guide](../docs/02-features/usage-guide.md).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Concurrent MCP stress test — real domains, real payloads, varying complexity.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node examples/mcp-bridge-concurrent.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { readFileSync } = require('node:fs');
|
|
12
|
+
const { createMCPBridge } = require('../src/mcp-bridge');
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
console.log('Connecting to barebrowse...');
|
|
16
|
+
const bridge = await createMCPBridge({ servers: ['barebrowse'], timeout: 15000 });
|
|
17
|
+
|
|
18
|
+
if (bridge.servers.length === 0) {
|
|
19
|
+
console.error('No servers connected');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const browse = bridge.tools.find(t => t.name === 'barebrowse_browse');
|
|
24
|
+
|
|
25
|
+
const tasks = [
|
|
26
|
+
{
|
|
27
|
+
label: 'Amazon NL — 2.5 inch HDD SATA case',
|
|
28
|
+
args: { url: 'https://www.amazon.nl/s?k=2.5+inch+hdd+sata+case', maxChars: 3000 },
|
|
29
|
+
verify: (r) => /amazon/i.test(r) || /hdd|sata|case|behuizing/i.test(r),
|
|
30
|
+
shouldFail: false,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: 'Wikipedia — Phoenician language',
|
|
34
|
+
args: { url: 'https://en.wikipedia.org/wiki/Phoenician_language', maxChars: 3000 },
|
|
35
|
+
verify: (r) => /phoenician/i.test(r) || /semitic|canaanite/i.test(r),
|
|
36
|
+
shouldFail: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Dead domain — should fail or timeout',
|
|
40
|
+
args: { url: 'https://this-domain-does-not-exist-xyz-999.com/page', maxChars: 1000 },
|
|
41
|
+
verify: () => true, // any response is fine, we just want to see it doesn't hang or crash others
|
|
42
|
+
shouldFail: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: 'GitHub — bare-agent repo',
|
|
46
|
+
args: { url: 'https://github.com/nicobailon/bareagent', maxChars: 2000 },
|
|
47
|
+
verify: (r) => /bare.?agent|orchestration|lightweight/i.test(r),
|
|
48
|
+
shouldFail: false,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: 'Slow static page — archive.org',
|
|
52
|
+
args: { url: 'https://web.archive.org/web/2024/https://example.com/', maxChars: 2000 },
|
|
53
|
+
verify: (r) => /example|wayback|archive/i.test(r),
|
|
54
|
+
shouldFail: false,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
console.log(`\nFiring ${tasks.length} concurrent browse calls...\n`);
|
|
59
|
+
|
|
60
|
+
const t0 = Date.now();
|
|
61
|
+
const results = await Promise.allSettled(
|
|
62
|
+
tasks.map(t => browse.execute(t.args))
|
|
63
|
+
);
|
|
64
|
+
const elapsed = Date.now() - t0;
|
|
65
|
+
|
|
66
|
+
let passed = 0;
|
|
67
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
68
|
+
const task = tasks[i];
|
|
69
|
+
const r = results[i];
|
|
70
|
+
const status = r.status === 'fulfilled' ? 'OK' : 'FAIL';
|
|
71
|
+
const value = r.status === 'fulfilled' ? r.value : r.reason.message;
|
|
72
|
+
let text = typeof value === 'string' ? value : JSON.stringify(value);
|
|
73
|
+
// If barebrowse saved to disk, read the file to verify actual content
|
|
74
|
+
const fileMatch = text.match(/saved to (.+\.yml)/);
|
|
75
|
+
if (fileMatch) {
|
|
76
|
+
try { text += '\n' + readFileSync(fileMatch[1], 'utf8'); } catch {}
|
|
77
|
+
}
|
|
78
|
+
const correct = r.status === 'fulfilled' && task.verify(text);
|
|
79
|
+
|
|
80
|
+
console.log(`[${i + 1}] ${task.label}`);
|
|
81
|
+
if (task.shouldFail) {
|
|
82
|
+
const handled = r.status === 'rejected' || (r.status === 'fulfilled' && /error|fail|not|ERR_/i.test(text.slice(0, 500)));
|
|
83
|
+
console.log(` Status: ${status} | Error handled gracefully: ${handled ? 'YES' : 'NO'}`);
|
|
84
|
+
console.log(` Preview: ${(r.status === 'rejected' ? r.reason.message : text).slice(0, 120)}...`);
|
|
85
|
+
console.log();
|
|
86
|
+
if (handled) passed++;
|
|
87
|
+
} else {
|
|
88
|
+
console.log(` Status: ${status} | Routed correctly: ${correct ? 'YES' : 'NO'}`);
|
|
89
|
+
console.log(` Size: ${text.length} chars`);
|
|
90
|
+
console.log(` Preview: ${text.slice(0, 120)}...`);
|
|
91
|
+
console.log();
|
|
92
|
+
if (correct) passed++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`--- Result: ${passed}/${tasks.length} routed correctly in ${elapsed}ms ---`);
|
|
97
|
+
console.log(passed === tasks.length ? 'PASS' : 'FAIL');
|
|
98
|
+
|
|
99
|
+
await bridge.close();
|
|
100
|
+
console.log('Closed.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main().catch(err => {
|
|
104
|
+
console.error(err);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MCP Bridge POC — auto-discover MCP servers, expose as bareagent tools, run Loop.
|
|
6
|
+
*
|
|
7
|
+
* First run: discovers servers from IDE configs, writes .mcp-bridge.json (all tools allowed).
|
|
8
|
+
* Edit .mcp-bridge.json to deny tools. Changes survive refresh.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* OPENAI_API_KEY=sk-... node examples/mcp-bridge-poc.js Go to example.com and describe it
|
|
12
|
+
* ANTHROPIC_API_KEY=sk-... node examples/mcp-bridge-poc.js --provider anthropic What tools do you have?
|
|
13
|
+
* node examples/mcp-bridge-poc.js --refresh # force re-discovery
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { Loop } = require('../src/loop');
|
|
17
|
+
const { createMCPBridge } = require('../src/mcp-bridge');
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
|
|
22
|
+
// Parse flags
|
|
23
|
+
const providerFlag = args.includes('--provider') ? args.splice(args.indexOf('--provider'), 2)[1] : 'openai';
|
|
24
|
+
const refresh = args.includes('--refresh') ? (args.splice(args.indexOf('--refresh'), 1), true) : false;
|
|
25
|
+
|
|
26
|
+
// Everything left is the prompt
|
|
27
|
+
const prompt = args.join(' ') || 'List all your available tools and describe what each one does.';
|
|
28
|
+
|
|
29
|
+
// Provider
|
|
30
|
+
let provider;
|
|
31
|
+
if (providerFlag === 'anthropic') {
|
|
32
|
+
const { AnthropicProvider } = require('../src/provider-anthropic');
|
|
33
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
34
|
+
if (!apiKey) { console.error('Set ANTHROPIC_API_KEY'); process.exit(1); }
|
|
35
|
+
provider = new AnthropicProvider({ apiKey, model: 'claude-sonnet-4-20250514' });
|
|
36
|
+
} else {
|
|
37
|
+
const { OpenAIProvider } = require('../src/provider-openai');
|
|
38
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
39
|
+
if (!apiKey) { console.error('Set OPENAI_API_KEY'); process.exit(1); }
|
|
40
|
+
provider = new OpenAIProvider({ apiKey, model: 'gpt-4.1-mini' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Bridge — reads .mcp-bridge.json if it exists, discovers on first run or TTL expiry
|
|
44
|
+
const bridge = await createMCPBridge({ refresh, timeout: 20000 });
|
|
45
|
+
|
|
46
|
+
if (bridge.servers.length === 0) {
|
|
47
|
+
console.error('No MCP servers found. Check your .mcp.json or ~/.claude/mcp_servers.json');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(`Prompt: ${prompt}`);
|
|
53
|
+
console.log('---');
|
|
54
|
+
|
|
55
|
+
const loop = new Loop({
|
|
56
|
+
provider,
|
|
57
|
+
maxRounds: 5,
|
|
58
|
+
system: `You are a helpful assistant with access to MCP tools. Use them to accomplish the user\'s request. Be concise.\n\n${bridge.systemContext}`,
|
|
59
|
+
onToolCall: (name, args) => console.log(`[tool] ${name}(${JSON.stringify(args)})`),
|
|
60
|
+
onText: (text) => console.log(`[response] ${text}`),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await loop.run(
|
|
65
|
+
[{ role: 'user', content: prompt }],
|
|
66
|
+
bridge.tools,
|
|
67
|
+
);
|
|
68
|
+
console.log('---');
|
|
69
|
+
console.log('Done.', { usage: result.usage, cost: result.cost ? `$${result.cost.toFixed(4)}` : 'n/a' });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('Loop error:', err.message);
|
|
72
|
+
} finally {
|
|
73
|
+
await bridge.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
main();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# orchestrator/
|
|
2
|
+
|
|
3
|
+
Reference pattern for multi-agent dispatch. **Three configs and a system
|
|
4
|
+
prompt — no orchestrator class, no role types, no DAG runner.** The LLM
|
|
5
|
+
itself is the dispatcher; the only primitive is `spawn(config, input)`.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
orchestrator/
|
|
9
|
+
├── orchestrator.json # the parent agent — picks a specialist per job
|
|
10
|
+
└── specialists/
|
|
11
|
+
├── summarizer.json # specialist: summarise text given as input
|
|
12
|
+
└── researcher.json # specialist: shell_grep + shell_read across cwd
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Run it
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd examples/orchestrator
|
|
19
|
+
OPENAI_API_KEY=... echo '{"content":"Summarise the README in this repo."}' \
|
|
20
|
+
| bare-agent --config orchestrator.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The orchestrator's system prompt tells the model:
|
|
24
|
+
|
|
25
|
+
> You receive a job. Decide which specialist handles it (summarizer or
|
|
26
|
+
> researcher), `spawn` that specialist with the relevant input, and return
|
|
27
|
+
> the specialist's result.
|
|
28
|
+
|
|
29
|
+
The model picks `spawn(config: 'specialists/researcher.json', input: ...)`,
|
|
30
|
+
the researcher reads the README via `shell_read`, returns its summary, the
|
|
31
|
+
orchestrator returns that to stdout.
|
|
32
|
+
|
|
33
|
+
## What's gated
|
|
34
|
+
|
|
35
|
+
Both `orchestrator.json` and `specialists/*.json` declare a `gate` block
|
|
36
|
+
that wires bareguard. Defaults:
|
|
37
|
+
|
|
38
|
+
- `budget.maxCostUsd: 0.50` — hard cap on the *family* (parent + children
|
|
39
|
+
share via `BAREGUARD_BUDGET_FILE`).
|
|
40
|
+
- `limits.maxTurns: 20`, `limits.maxChildren: 3`, `limits.maxDepth: 2` —
|
|
41
|
+
spawn-tree shape bounds.
|
|
42
|
+
- `spawn.ratePerMinute: 5`, `defer.ratePerMinute: 10` — bareguard 0.2 rate
|
|
43
|
+
caps.
|
|
44
|
+
- `bash.allow` and `fs.readScope` scoped per specialist.
|
|
45
|
+
|
|
46
|
+
## Why this isn't a framework
|
|
47
|
+
|
|
48
|
+
There's no `class Orchestrator`, no `dispatch_to_specialist()`, no shared
|
|
49
|
+
state object. Roles are *configs*, not types. Adding a new specialist is
|
|
50
|
+
adding one JSON file — no code change to bareagent or the orchestrator.
|
|
51
|
+
|
|
52
|
+
The "intelligence" is the orchestrator's system prompt. Substitute a
|
|
53
|
+
better-written prompt and the same primitives handle a 5-specialist team.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"systemPrompt": "You are an orchestrator. You receive a job (the user's first message), decide which specialist handles it, then spawn that specialist with the relevant input and return its result.\n\nAvailable specialists (config paths relative to cwd):\n - specialists/summarizer.json — summarises text content given via input.content\n - specialists/researcher.json — searches and reads files in the project\n\nUse the spawn tool: spawn({ config: '<path>', input: { ...whatever the specialist expects... } }). The result.text from spawn is the specialist's final answer — return it verbatim or with a one-line preface.\n\nDo not attempt the work yourself; you are a router. If no specialist fits, say so plainly.",
|
|
3
|
+
"provider": "openai",
|
|
4
|
+
"model": "gpt-4o-mini",
|
|
5
|
+
"tools": ["spawn"],
|
|
6
|
+
"gate": {
|
|
7
|
+
"budget": { "maxCostUsd": 0.50 },
|
|
8
|
+
"limits": { "maxTurns": 20, "maxChildren": 3, "maxDepth": 2 },
|
|
9
|
+
"spawn": { "ratePerMinute": 5 },
|
|
10
|
+
"defer": { "ratePerMinute": 10 },
|
|
11
|
+
"tools": { "allowlist": ["spawn"] },
|
|
12
|
+
"audit": { "path": "./bareagent-audit.jsonl" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"systemPrompt": "You are a researcher. You have shell_read and shell_grep tools scoped to the current working directory. The user's message describes what to find or read. Use shell_grep to locate matches and shell_read to read whole files. Return a concise summary of what you found (3-8 sentences). Do not invent file contents; if you can't find something, say so.",
|
|
3
|
+
"provider": "openai",
|
|
4
|
+
"model": "gpt-4o-mini",
|
|
5
|
+
"tools": ["shell_read", "shell_grep"],
|
|
6
|
+
"gate": {
|
|
7
|
+
"limits": { "maxTurns": 10 },
|
|
8
|
+
"fs": { "readScope": ["."] },
|
|
9
|
+
"tools": { "allowlist": ["shell_read", "shell_grep"] },
|
|
10
|
+
"audit": { "path": "./bareagent-audit.jsonl" }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"systemPrompt": "You are a summarizer. The user's message contains text to summarise. Produce a concise (3-5 sentence) summary capturing the main points. Return the summary as your final response — no tool calls needed unless the input is malformed.",
|
|
3
|
+
"provider": "openai",
|
|
4
|
+
"model": "gpt-4o-mini",
|
|
5
|
+
"tools": [],
|
|
6
|
+
"gate": {
|
|
7
|
+
"limits": { "maxTurns": 5 },
|
|
8
|
+
"tools": { "allowlist": [] },
|
|
9
|
+
"audit": { "path": "./bareagent-audit.jsonl" }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Replay-job POC — supervised replay of a recorded browser task.
|
|
6
|
+
*
|
|
7
|
+
* The idea: record once with the LLM driving (full reasoning), then on every
|
|
8
|
+
* subsequent run replay the recorded *intents* against a fresh snapshot, with
|
|
9
|
+
* the LLM acting as a locator/supervisor — not a planner. If the locator
|
|
10
|
+
* can't find a match for a step, fall back to full Loop reasoning from that
|
|
11
|
+
* point and overwrite the trace. That's the self-healing path.
|
|
12
|
+
*
|
|
13
|
+
* Why this isn't a barebrowse feature: barebrowse stays dumb (URL → snapshot,
|
|
14
|
+
* ref → action). The "job" concept (trace storage, replay supervisor, scheduler
|
|
15
|
+
* hookup) is composed from bareagent primitives.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* # First run — record:
|
|
19
|
+
* OPENAI_API_KEY=sk-... node examples/replay-job.js --record demo-job \
|
|
20
|
+
* "Go to example.com and click the 'More information' link"
|
|
21
|
+
*
|
|
22
|
+
* # Subsequent runs — replay:
|
|
23
|
+
* OPENAI_API_KEY=sk-... node examples/replay-job.js --replay demo-job
|
|
24
|
+
*
|
|
25
|
+
* # Cron'd:
|
|
26
|
+
* *\/15 * * * * node /path/to/replay-job.js --replay demo-job
|
|
27
|
+
*
|
|
28
|
+
* What's deliberately NOT in this POC (next steps, in order):
|
|
29
|
+
* 1. Fingerprint fast-path: hash selector+role+text per step, try direct
|
|
30
|
+
* match before calling the locator LLM. Brings per-step cost to ~0 on
|
|
31
|
+
* stable UIs (Gmail, IG).
|
|
32
|
+
* 2. PostState assertion: after each step, ask the LLM "does this snapshot
|
|
33
|
+
* reflect the expected outcome?" Without this, replay can silently drift
|
|
34
|
+
* on subtly-changed UIs.
|
|
35
|
+
* 3. Trace confidence: rolling success rate per step; below a threshold,
|
|
36
|
+
* re-derive the whole trace instead of patching one entry.
|
|
37
|
+
* 4. Scheduler integration: wrap as a Scheduler trigger so cron is a config
|
|
38
|
+
* line instead of an OS cron entry.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const fs = require('node:fs');
|
|
42
|
+
const path = require('node:path');
|
|
43
|
+
const { Loop } = require('../src/loop');
|
|
44
|
+
|
|
45
|
+
const JOBS_DIR = path.join(__dirname, '..', '.jobs');
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const mode = argv.includes('--record') ? 'record'
|
|
49
|
+
: argv.includes('--replay') ? 'replay'
|
|
50
|
+
: null;
|
|
51
|
+
const flagIdx = argv.indexOf(`--${mode}`);
|
|
52
|
+
const name = mode ? argv[flagIdx + 1] : null;
|
|
53
|
+
const goal = argv.slice(flagIdx + 2).join(' ');
|
|
54
|
+
return { mode, name, goal };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function jobPath(name) { return path.join(JOBS_DIR, `${name}.json`); }
|
|
58
|
+
|
|
59
|
+
function loadProvider() {
|
|
60
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
61
|
+
if (!apiKey) { console.error('Set OPENAI_API_KEY'); process.exit(1); }
|
|
62
|
+
const { OpenAIProvider } = require('../src/provider-openai');
|
|
63
|
+
return new OpenAIProvider({ apiKey, model: 'gpt-4.1-mini' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadBrowseTools() {
|
|
67
|
+
const mod = await import('barebrowse/bareagent');
|
|
68
|
+
return mod.createBrowseTools({});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- RECORD ----------------------------------------------------------------
|
|
72
|
+
// Run Loop normally. The onToolCall hook captures each step as it happens.
|
|
73
|
+
// The trace stores the LLM's free-text "intent" (the assistant message
|
|
74
|
+
// immediately preceding the tool call) alongside the tool name and args.
|
|
75
|
+
// On replay, intent is what the locator LLM matches against — not args.
|
|
76
|
+
async function record({ name, goal }) {
|
|
77
|
+
const provider = loadProvider();
|
|
78
|
+
const { tools, close } = await loadBrowseTools();
|
|
79
|
+
|
|
80
|
+
const trace = [];
|
|
81
|
+
let pendingIntent = '';
|
|
82
|
+
|
|
83
|
+
const loop = new Loop({
|
|
84
|
+
provider,
|
|
85
|
+
maxRounds: 15,
|
|
86
|
+
onText: (text) => { pendingIntent = text; },
|
|
87
|
+
onToolCall: (toolName, args) => {
|
|
88
|
+
trace.push({ intent: pendingIntent.trim().slice(0, 500), tool: toolName, args });
|
|
89
|
+
pendingIntent = '';
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const result = await loop.run([{ role: 'user', content: goal }], tools);
|
|
95
|
+
fs.mkdirSync(JOBS_DIR, { recursive: true });
|
|
96
|
+
fs.writeFileSync(jobPath(name), JSON.stringify({ goal, recordedAt: new Date().toISOString(), trace }, null, 2));
|
|
97
|
+
console.log(`[record] saved ${trace.length} steps to ${jobPath(name)}`);
|
|
98
|
+
console.log(`[record] cost: $${(result.cost ?? 0).toFixed(4)}`);
|
|
99
|
+
} finally {
|
|
100
|
+
await close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- REPLAY ----------------------------------------------------------------
|
|
105
|
+
// For each recorded step:
|
|
106
|
+
// 1. Take a fresh snapshot.
|
|
107
|
+
// 2. Ask the LLM (no tools, JSON-mode) to map the recorded intent to a ref
|
|
108
|
+
// in the current snapshot — OR return null if no match.
|
|
109
|
+
// 3. On match: execute the tool with the resolved ref. The recorded args
|
|
110
|
+
// that aren't refs (e.g. type's `text`, goto's `url`) carry over verbatim.
|
|
111
|
+
// 4. On miss: fall back to driving Loop from the remaining goal, capture
|
|
112
|
+
// the new sub-trace, and splice it into the saved trace.
|
|
113
|
+
async function replay({ name }) {
|
|
114
|
+
const job = JSON.parse(fs.readFileSync(jobPath(name), 'utf8'));
|
|
115
|
+
const provider = loadProvider();
|
|
116
|
+
const { tools, close } = await loadBrowseTools();
|
|
117
|
+
const toolByName = Object.fromEntries(tools.map((t) => [t.name, t]));
|
|
118
|
+
|
|
119
|
+
let mutated = false;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
for (let i = 0; i < job.trace.length; i++) {
|
|
123
|
+
const step = job.trace[i];
|
|
124
|
+
const tool = toolByName[step.tool];
|
|
125
|
+
if (!tool) throw new Error(`unknown tool in trace: ${step.tool}`);
|
|
126
|
+
|
|
127
|
+
// Steps whose args have no ref (goto, browse, back, scroll) replay verbatim.
|
|
128
|
+
if (!('ref' in (step.args || {}))) {
|
|
129
|
+
console.log(`[replay ${i + 1}/${job.trace.length}] ${step.tool}(${JSON.stringify(step.args)})`);
|
|
130
|
+
await tool.execute(step.args);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Ref-bearing steps: snapshot, then ask the LLM to locate.
|
|
135
|
+
const snapshot = await toolByName.snapshot.execute({});
|
|
136
|
+
const snapshotText = typeof snapshot === 'string' ? snapshot : (snapshot?.snapshot ?? JSON.stringify(snapshot));
|
|
137
|
+
const located = await locate({ provider, intent: step.intent, tool: step.tool, snapshotText });
|
|
138
|
+
|
|
139
|
+
if (located.ref) {
|
|
140
|
+
const args = { ...step.args, ref: located.ref };
|
|
141
|
+
console.log(`[replay ${i + 1}/${job.trace.length}] ${step.tool}(ref=${located.ref}) — intent="${step.intent.slice(0, 60)}"`);
|
|
142
|
+
await tool.execute(args);
|
|
143
|
+
} else {
|
|
144
|
+
console.warn(`[replay ${i + 1}/${job.trace.length}] locator miss — falling back to full Loop`);
|
|
145
|
+
const remaining = `Continue this task from the current page. Original goal: ${job.goal}. We have completed ${i} of ${job.trace.length} recorded steps; the next intended step was: "${step.intent}".`;
|
|
146
|
+
const subTrace = await driveFallback({ provider, tools, goal: remaining });
|
|
147
|
+
job.trace.splice(i, job.trace.length - i, ...subTrace);
|
|
148
|
+
mutated = true;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (mutated) {
|
|
154
|
+
job.recordedAt = new Date().toISOString();
|
|
155
|
+
fs.writeFileSync(jobPath(name), JSON.stringify(job, null, 2));
|
|
156
|
+
console.log(`[replay] trace patched after locator miss; saved ${job.trace.length} steps`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`[replay] completed ${job.trace.length} steps without falling back`);
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
await close();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Locator: structured-output call, no tools, no loop. One LLM call per ref-bearing step.
|
|
166
|
+
async function locate({ provider, intent, tool, snapshotText }) {
|
|
167
|
+
const system = 'You map a recorded intent to a ref in a current ARIA snapshot. Reply with strict JSON: {"ref": "<ref>"} if confident, or {"ref": null, "reason": "<why>"} if no element matches. Never invent refs not present in the snapshot.';
|
|
168
|
+
const user = `Recorded intent: ${intent}\nTool: ${tool}\n\nCurrent snapshot:\n${snapshotText}\n\nReturn JSON only.`;
|
|
169
|
+
const { text } = await provider.generate({
|
|
170
|
+
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
174
|
+
return JSON.parse(match ? match[0] : text);
|
|
175
|
+
} catch {
|
|
176
|
+
return { ref: null, reason: 'unparseable locator response' };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Fallback driver: same shape as record(), just used mid-replay when the
|
|
181
|
+
// locator can't resolve a step. Returns the new sub-trace to splice in.
|
|
182
|
+
async function driveFallback({ provider, tools, goal }) {
|
|
183
|
+
const sub = [];
|
|
184
|
+
let pendingIntent = '';
|
|
185
|
+
const loop = new Loop({
|
|
186
|
+
provider,
|
|
187
|
+
maxRounds: 10,
|
|
188
|
+
onText: (text) => { pendingIntent = text; },
|
|
189
|
+
onToolCall: (toolName, args) => {
|
|
190
|
+
sub.push({ intent: pendingIntent.trim().slice(0, 500), tool: toolName, args });
|
|
191
|
+
pendingIntent = '';
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
await loop.run([{ role: 'user', content: goal }], tools);
|
|
195
|
+
return sub;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function main() {
|
|
199
|
+
const { mode, name, goal } = parseArgs(process.argv.slice(2));
|
|
200
|
+
if (!mode || !name) {
|
|
201
|
+
console.error('Usage:\n --record <name> "<goal>"\n --replay <name>');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
if (mode === 'record') {
|
|
205
|
+
if (!goal) { console.error('record mode requires a goal string'); process.exit(1); }
|
|
206
|
+
await record({ name, goal });
|
|
207
|
+
} else {
|
|
208
|
+
if (!fs.existsSync(jobPath(name))) { console.error(`no job at ${jobPath(name)}`); process.exit(1); }
|
|
209
|
+
await replay({ name });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main().catch((err) => { console.error(err); process.exit(1); });
|
package/examples/wake.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# wake.sh — defer queue runner
|
|
2
|
+
|
|
3
|
+
`examples/wake.sh` is the reference scheduler that fires bareagent's
|
|
4
|
+
deferred actions. It's not a library primitive — it's a small bash script
|
|
5
|
+
you copy into your project and adapt. Bareagent emits JSONL records via
|
|
6
|
+
the `defer` tool; wake.sh reads the queue and re-invokes bareagent with
|
|
7
|
+
the fired action.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cp examples/wake.sh /usr/local/bin/bareagent-wake
|
|
13
|
+
chmod +x /usr/local/bin/bareagent-wake
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Cron entry (every minute)
|
|
17
|
+
|
|
18
|
+
```cron
|
|
19
|
+
* * * * * /usr/local/bin/bareagent-wake
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For project-scoped use, run from the project directory:
|
|
23
|
+
|
|
24
|
+
```cron
|
|
25
|
+
* * * * * cd /path/to/your/project && /usr/local/bin/bareagent-wake
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Environment overrides
|
|
29
|
+
|
|
30
|
+
| Variable | Default | What it does |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `BAREAGENT_DEFER_QUEUE` | `./bareagent-defers.jsonl` | Path to the JSONL defer queue (must match what the `defer` tool writes). |
|
|
33
|
+
| `ORCHESTRATOR_CONFIG` | `./orchestrator.json` | Bareagent config file the wake script invokes for fired actions. |
|
|
34
|
+
| `LOCKFILE` | `/tmp/bareagent-wake.lock` | Single-instance lock via `flock`. |
|
|
35
|
+
| `BAREAGENT_WAKE_LOG_DIR` | `/tmp/bareagent-wake` | Per-fired-action log directory. |
|
|
36
|
+
|
|
37
|
+
## Dependencies
|
|
38
|
+
|
|
39
|
+
- `jq` — JSONL fold + filter
|
|
40
|
+
- `flock` (Linux util-linux) — single-instance lock
|
|
41
|
+
- `bare-agent` on `$PATH` — `npm install -g bare-agent` or use the full path
|
|
42
|
+
|
|
43
|
+
## Behaviour
|
|
44
|
+
|
|
45
|
+
1. **Folds** the queue: `{id, status, ...}` records are append-only; the
|
|
46
|
+
live status of each id is the *latest* line. jq does the fold.
|
|
47
|
+
2. **Filters** to `status === 'pending' AND when <= now()`.
|
|
48
|
+
3. For each due record: appends `{id, status: 'fired', ts}` (atomic JSONL
|
|
49
|
+
append on POSIX), then invokes
|
|
50
|
+
`bare-agent --config $ORCHESTRATOR_CONFIG` with the inner action as
|
|
51
|
+
stdin input. Bareagent runs the action through bareguard's gate as a
|
|
52
|
+
fresh action — full pipeline against the inner action, separate audit
|
|
53
|
+
line.
|
|
54
|
+
4. After the fired invocation completes: appends `{id, status: 'done|failed', ts, exit_code?}`.
|
|
55
|
+
|
|
56
|
+
## Why bash and not Node
|
|
57
|
+
|
|
58
|
+
The wake script is OS-level glue — cron + filesystem + subprocess. Keeping
|
|
59
|
+
it as a shell script makes the dependency on bareagent (and only bareagent)
|
|
60
|
+
obvious, and avoids users thinking the script is a library to import.
|
|
61
|
+
|
|
62
|
+
## Customisation points
|
|
63
|
+
|
|
64
|
+
- **Different queue path:** set `BAREAGENT_DEFER_QUEUE` and pass the same
|
|
65
|
+
to your `defer` tool config (or `BAREAGENT_DEFER_QUEUE` env on the
|
|
66
|
+
bareagent process that emits).
|
|
67
|
+
- **Different orchestrator per action type:** parse `record.action.type`
|
|
68
|
+
and pick a config file accordingly. ~5 lines added inside the per-record
|
|
69
|
+
loop.
|
|
70
|
+
- **Different fire-time semantics:** instead of invoking bareagent CLI,
|
|
71
|
+
shell out to a Node script that wires Loop differently. The defer queue
|
|
72
|
+
schema doesn't constrain you.
|
|
73
|
+
|
|
74
|
+
## Log rotation
|
|
75
|
+
|
|
76
|
+
`logrotate(8)` is the standard answer. Example
|
|
77
|
+
`/etc/logrotate.d/bareagent-wake`:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
/tmp/bareagent-wake/*.log {
|
|
81
|
+
daily
|
|
82
|
+
rotate 7
|
|
83
|
+
compress
|
|
84
|
+
missingok
|
|
85
|
+
notifempty
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/path/to/your/project/bareagent-defers.jsonl {
|
|
89
|
+
weekly
|
|
90
|
+
rotate 4
|
|
91
|
+
compress
|
|
92
|
+
missingok
|
|
93
|
+
notifempty
|
|
94
|
+
copytruncate
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`copytruncate` matters for the queue: it preserves the file inode (which
|
|
99
|
+
the defer tool's `appendFile` depends on for atomic POSIX appends).
|
package/examples/wake.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# examples/wake.sh — reference scheduler for bareagent's defer queue.
|
|
3
|
+
#
|
|
4
|
+
# This is a *reference*, not a primitive. Copy into your project and modify.
|
|
5
|
+
# See examples/wake.md for the cron entry and customization points.
|
|
6
|
+
#
|
|
7
|
+
# What this script does:
|
|
8
|
+
# 1. Reads the JSONL defer queue file.
|
|
9
|
+
# 2. Folds status-update lines per id (latest wins) using jq.
|
|
10
|
+
# 3. For each pending record whose `when` <= now: appends a "fired" status
|
|
11
|
+
# line to the queue (atomic JSONL append), then invokes
|
|
12
|
+
# `bareagent --config <orchestrator>` with the inner action as stdin.
|
|
13
|
+
# 4. Uses flock(1) to prevent overlapping wake invocations.
|
|
14
|
+
#
|
|
15
|
+
# The fired action goes through bareguard's gate AGAIN at fire time — full
|
|
16
|
+
# pipeline against the inner action, separate from the emit-time check.
|
|
17
|
+
# (Two gate.check calls, two distinct audit lines, reconstructable via
|
|
18
|
+
# parent_run_id.)
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
QUEUE="${BAREAGENT_DEFER_QUEUE:-./bareagent-defers.jsonl}"
|
|
23
|
+
ORCHESTRATOR_CONFIG="${ORCHESTRATOR_CONFIG:-./orchestrator.json}"
|
|
24
|
+
LOCKFILE="${LOCKFILE:-/tmp/bareagent-wake.lock}"
|
|
25
|
+
LOG_DIR="${BAREAGENT_WAKE_LOG_DIR:-/tmp/bareagent-wake}"
|
|
26
|
+
|
|
27
|
+
mkdir -p "$LOG_DIR"
|
|
28
|
+
|
|
29
|
+
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
30
|
+
|
|
31
|
+
# Single-instance: bail if another wake is running.
|
|
32
|
+
exec 9>"$LOCKFILE"
|
|
33
|
+
if ! flock -n 9; then
|
|
34
|
+
echo "[wake $NOW] another instance running, exiting" >&2
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# No queue file = nothing to do.
|
|
39
|
+
if [ ! -f "$QUEUE" ]; then
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Reconstruct status by folding all lines per id (latest wins) and filter
|
|
44
|
+
# to records whose `when` <= now AND status == "pending". One JSON object
|
|
45
|
+
# per output line.
|
|
46
|
+
PENDING=$(jq -n -c '
|
|
47
|
+
reduce inputs as $r ({};
|
|
48
|
+
.[$r.id] |= (. // {}) + $r
|
|
49
|
+
)
|
|
50
|
+
| to_entries
|
|
51
|
+
| map(.value)
|
|
52
|
+
| map(select(.status == "pending" and .when <= "'"$NOW"'"))
|
|
53
|
+
| .[]
|
|
54
|
+
' < "$QUEUE")
|
|
55
|
+
|
|
56
|
+
if [ -z "$PENDING" ]; then
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo "$PENDING" | while IFS= read -r record; do
|
|
61
|
+
[ -z "$record" ] && continue
|
|
62
|
+
|
|
63
|
+
ID=$(echo "$record" | jq -r '.id')
|
|
64
|
+
ACTION=$(echo "$record" | jq -c '.action')
|
|
65
|
+
|
|
66
|
+
# Append "fired" status line first (defer queue is append-only).
|
|
67
|
+
printf '{"id":"%s","status":"fired","ts":"%s"}\n' "$ID" "$NOW" >> "$QUEUE"
|
|
68
|
+
|
|
69
|
+
# Invoke bareagent with the deferred action as stdin input.
|
|
70
|
+
# Run in background — wake script doesn't wait for completion.
|
|
71
|
+
( echo "$ACTION" | bare-agent --config "$ORCHESTRATOR_CONFIG" \
|
|
72
|
+
>> "$LOG_DIR/fired-$ID.log" 2>&1
|
|
73
|
+
rc=$?
|
|
74
|
+
if [ $rc -ne 0 ]; then
|
|
75
|
+
printf '{"id":"%s","status":"failed","ts":"%s","exit_code":%d}\n' \
|
|
76
|
+
"$ID" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$rc" >> "$QUEUE"
|
|
77
|
+
else
|
|
78
|
+
printf '{"id":"%s","status":"done","ts":"%s"}\n' \
|
|
79
|
+
"$ID" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$QUEUE"
|
|
80
|
+
fi
|
|
81
|
+
) &
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
wait
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// examples/with-bareguard.mjs
|
|
2
|
+
//
|
|
3
|
+
// End-to-end: bareagent Loop + bareguard Gate.
|
|
4
|
+
// Runs a small LLM loop with budget cap, fs scope, audit log, and humanChannel.
|
|
5
|
+
//
|
|
6
|
+
// Run: OPENAI_API_KEY=... node examples/with-bareguard.mjs
|
|
7
|
+
//
|
|
8
|
+
// What this demonstrates:
|
|
9
|
+
// - Single-gate governance: every tool call traverses gate.check; every
|
|
10
|
+
// result reaches gate.record (via wrapTools).
|
|
11
|
+
// - Budget halt: if accumulated cost exceeds maxCostUsd, gate halts the loop.
|
|
12
|
+
// - Audit log: one JSONL line per gated event at ./bareagent-audit.jsonl.
|
|
13
|
+
// - humanChannel: required by bareguard. Here we auto-deny asks; in real use
|
|
14
|
+
// wire it to a chat platform, terminal prompt, etc.
|
|
15
|
+
|
|
16
|
+
import { Gate } from 'bareguard';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const { Loop, wireGate } = require('bare-agent');
|
|
20
|
+
const { OpenAI } = require('bare-agent/providers');
|
|
21
|
+
const { createShellTools } = require('bare-agent/tools');
|
|
22
|
+
|
|
23
|
+
// 1. Build the gate. Every primitive is optional with sensible defaults.
|
|
24
|
+
const gate = new Gate({
|
|
25
|
+
budget: { maxCostUsd: 0.10 }, // hard USD cap
|
|
26
|
+
limits: { maxTurns: 20 }, // safety net on think/act cycles
|
|
27
|
+
fs: { readScope: ['/tmp', '~/'] }, // shell_read / shell_grep allowed roots
|
|
28
|
+
bash: { allow: ['ls', 'cat', 'echo', 'pwd'] }, // argv[0] allowlist for shell_run
|
|
29
|
+
audit: { path: './bareagent-audit.jsonl' },
|
|
30
|
+
// Required by bareguard: any ask/halt event flows through here.
|
|
31
|
+
// Auto-deny is the safest default for headless use; in real apps, wire to
|
|
32
|
+
// a Telegram/Slack/terminal prompt and return { decision: 'allow' | 'deny' }.
|
|
33
|
+
humanChannel: async (event) => {
|
|
34
|
+
console.warn(`[humanChannel] ${event.kind}: ${event.rule} — auto-denying`);
|
|
35
|
+
return { decision: 'deny' };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
await gate.init();
|
|
39
|
+
|
|
40
|
+
// 2. Wire the gate into Loop's policy slot and wrap tools so gate.record fires.
|
|
41
|
+
const { policy, wrapTools } = wireGate(gate);
|
|
42
|
+
|
|
43
|
+
// 3. Standard bareagent setup.
|
|
44
|
+
const provider = new OpenAI({
|
|
45
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
46
|
+
model: 'gpt-4o-mini',
|
|
47
|
+
});
|
|
48
|
+
const { tools } = createShellTools();
|
|
49
|
+
|
|
50
|
+
const loop = new Loop({
|
|
51
|
+
provider,
|
|
52
|
+
policy,
|
|
53
|
+
onError: (err, meta) => console.error(`[onError ${meta.source}]`, err.message),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 4. Run.
|
|
57
|
+
const result = await loop.run(
|
|
58
|
+
[{ role: 'user', content: 'List the contents of /tmp using shell_run with argv ["ls", "/tmp"].' }],
|
|
59
|
+
wrapTools(tools),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console.log('---');
|
|
63
|
+
console.log('text:', result.text);
|
|
64
|
+
console.log('cost:', result.cost?.toFixed(6) ?? 'n/a');
|
|
65
|
+
console.log('audit log → ./bareagent-audit.jsonl');
|