orchestra-agents 1.0.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/LICENSE +21 -0
- package/README.md +72 -0
- package/orchestra-max.mjs +219 -0
- package/orchestra.mjs +277 -0
- package/package.json +41 -0
- package/roles.mjs +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kirk Kodre
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Orchestra
|
|
2
|
+
|
|
3
|
+
A multi-agent system in one file. One conductor agent takes your goal, breaks it into independent tasks, fans out specialist sub-agents in parallel (scouts, analysts, writers), sends adversarial critics to attack their work, then synthesizes everything into one final deliverable.
|
|
4
|
+
|
|
5
|
+
Built on the Claude API (`@anthropic-ai/sdk`). No framework. This is the "main agent talks to a bunch of sub-agents" pattern, owned end to end in plain JavaScript you can read in one sitting.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
git clone https://github.com/vyn-store/orchestra-agents.git
|
|
11
|
+
cd orchestra-agents
|
|
12
|
+
npm install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Auth: set the `ANTHROPIC_API_KEY` env var or copy `.env.example` to `.env` and paste your key.
|
|
16
|
+
|
|
17
|
+
## Run it
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
node orchestra.mjs "your goal here"
|
|
21
|
+
node orchestra.mjs "research the AI voice receptionist market for law firms and write me a one-page pitch" --agents 5 --web
|
|
22
|
+
node orchestra.mjs "quick test" --cheap # haiku everywhere, costs pennies
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Flag | What it does |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `--agents N` | max sub-agents the conductor may cast (default 5) |
|
|
28
|
+
| `--web` | scouts get live web search (server-side tool, Opus/Sonnet 4.6+) |
|
|
29
|
+
| `--cheap` | haiku-4-5 for everything (smoke tests) |
|
|
30
|
+
| `--model M` | conductor + synthesis model (default claude-opus-4-8) |
|
|
31
|
+
| `--worker M` | sub-agent model (default: same as --model) |
|
|
32
|
+
| `--no-critics` | skip the adversarial pass |
|
|
33
|
+
|
|
34
|
+
## How it works (the 4 phases)
|
|
35
|
+
|
|
36
|
+
1. **PLAN**: the conductor gets the goal and returns a structured JSON plan (enforced by the API's structured outputs, so it can never come back malformed): a strategy plus one self-contained prompt per sub-agent.
|
|
37
|
+
2. **EXECUTE**: sub-agents run in parallel (concurrency-capped). Each gets ONE role system prompt from `roles.mjs`: scout finds facts, analyst reasons, writer produces the deliverable.
|
|
38
|
+
3. **CRITIQUE**: one adversarial critic per result, prompted to assume the work is wrong until proven right. Returns a structured score + fixes.
|
|
39
|
+
4. **SYNTHESIZE**: the conductor merges everything, applies the critics' fixes, drops anything scored under 5, and streams the final deliverable live to your terminal.
|
|
40
|
+
|
|
41
|
+
Every run prints a cost meter (real token counts x current pricing) and saves a full transcript to `runs/`.
|
|
42
|
+
|
|
43
|
+
## Why it's built this way
|
|
44
|
+
|
|
45
|
+
- **Independent context per sub-agent**: critics that never saw the original reasoning refute better.
|
|
46
|
+
- **Structured outputs, not prompt-and-pray**: the plan and verdicts are schema-enforced by the API.
|
|
47
|
+
- **The orchestrator is plain code**: casting, thresholds, and loops live in JS; agents discover, code decides.
|
|
48
|
+
- **Cost is visible**: you see every dollar as it happens.
|
|
49
|
+
|
|
50
|
+
## Two engines, same orchestra
|
|
51
|
+
|
|
52
|
+
| | `orchestra.mjs` | `orchestra-max.mjs` |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Runs on | Anthropic API (your key) | **Your Claude subscription** (headless Claude Code) |
|
|
55
|
+
| Costs | API credits per token | $0 credits; counts against your plan's usage limits |
|
|
56
|
+
| Speed | Faster per agent | Slower per agent (each is a full Claude Code session) |
|
|
57
|
+
| Web for scouts | Server-side web search | Claude Code's WebSearch/WebFetch tools |
|
|
58
|
+
| When to use | Client demos / production | Your own daily runs, anything personal |
|
|
59
|
+
|
|
60
|
+
Same flags on both. If your API credits are empty, `orchestra-max.mjs` keeps working. It needs [Claude Code](https://claude.com/claude-code) installed and logged in, nothing else.
|
|
61
|
+
|
|
62
|
+
## Customize it
|
|
63
|
+
|
|
64
|
+
The whole system is three small files:
|
|
65
|
+
|
|
66
|
+
- `orchestra.mjs`: the conductor, the phases, the cost meter.
|
|
67
|
+
- `orchestra-max.mjs`: same conductor, but every sub-agent is a `claude -p` session.
|
|
68
|
+
- `roles.mjs`: the sub-agent system prompts. Add your own roles here (a coder, a translator, a fact-checker) and the conductor can cast them.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ORCHESTRA MAX — the same multi-agent orchestra, running on your Claude
|
|
3
|
+
// subscription instead of API credits. Every sub-agent is a headless Claude
|
|
4
|
+
// Code session (`claude -p`), authenticated by your Claude Max login.
|
|
5
|
+
// Zero API credits used. Usage counts against your Max plan limits instead.
|
|
6
|
+
//
|
|
7
|
+
// node orchestra-max.mjs "your goal here"
|
|
8
|
+
// node orchestra-max.mjs "your goal" --agents 4 --web
|
|
9
|
+
// node orchestra-max.mjs "your goal" --cheap (haiku everywhere)
|
|
10
|
+
//
|
|
11
|
+
// Flags:
|
|
12
|
+
// --agents N max sub-agents the conductor may cast (default 5)
|
|
13
|
+
// --web scouts may use WebSearch/WebFetch (Claude Code built-in tools)
|
|
14
|
+
// --cheap haiku for everything (fast smoke tests)
|
|
15
|
+
// --model M conductor + synthesis model (default: opus)
|
|
16
|
+
// --worker M sub-agent model (default: sonnet - lighter on plan limits)
|
|
17
|
+
// --no-critics skip the adversarial pass
|
|
18
|
+
//
|
|
19
|
+
// Requires: the `claude` CLI logged in to your subscription. No API key needed
|
|
20
|
+
// (an ANTHROPIC_API_KEY in the environment is deliberately stripped so runs
|
|
21
|
+
// can never silently bill your credit balance).
|
|
22
|
+
|
|
23
|
+
import { execFile } from "node:child_process";
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { promisify } from "node:util";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { ROLES } from "./roles.mjs";
|
|
29
|
+
|
|
30
|
+
const run = promisify(execFile);
|
|
31
|
+
const DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const AGENT_HOME = path.join(DIR, "agent-home"); // empty cwd so agents don't inherit a project CLAUDE.md
|
|
33
|
+
fs.mkdirSync(AGENT_HOME, { recursive: true });
|
|
34
|
+
|
|
35
|
+
// ---------- config ----------
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
const flag = (n) => args.includes(n);
|
|
38
|
+
const opt = (n, d) => {
|
|
39
|
+
const i = args.indexOf(n);
|
|
40
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : d;
|
|
41
|
+
};
|
|
42
|
+
const valueFlags = ["--agents", "--model", "--worker"];
|
|
43
|
+
const GOAL = args.filter((a, i) => !a.startsWith("--") && !valueFlags.includes(args[i - 1])).join(" ").trim();
|
|
44
|
+
|
|
45
|
+
const CHEAP = flag("--cheap");
|
|
46
|
+
const CONDUCTOR_MODEL = CHEAP ? "haiku" : opt("--model", "opus");
|
|
47
|
+
const WORKER_MODEL = CHEAP ? "haiku" : opt("--worker", "sonnet");
|
|
48
|
+
const MAX_AGENTS = parseInt(opt("--agents", "5"), 10);
|
|
49
|
+
const USE_WEB = flag("--web");
|
|
50
|
+
const USE_CRITICS = !flag("--no-critics");
|
|
51
|
+
const CONCURRENCY = 3;
|
|
52
|
+
|
|
53
|
+
// ---------- helpers ----------
|
|
54
|
+
const C = { dim: "\x1b[2m", cyan: "\x1b[36m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", bold: "\x1b[1m", reset: "\x1b[0m" };
|
|
55
|
+
const log = (color, tag, msg) => console.log(`${C[color]}${C.bold}[${tag}]${C.reset} ${msg}`);
|
|
56
|
+
|
|
57
|
+
const usage = { calls: 0, in: 0, out: 0, nominal: 0 };
|
|
58
|
+
|
|
59
|
+
// One headless Claude Code session = one agent. Retries once on transient
|
|
60
|
+
// failures (the API SDK auto-retries overloads; execFile does not, so we do).
|
|
61
|
+
async function claudeAgent({ prompt, system, model, tools = [] }, attempt = 1) {
|
|
62
|
+
const cliArgs = ["-p", prompt, "--output-format", "json", "--model", model];
|
|
63
|
+
if (system) cliArgs.push("--append-system-prompt", system);
|
|
64
|
+
if (tools.length) cliArgs.push("--allowedTools", tools.join(","));
|
|
65
|
+
const env = { ...process.env };
|
|
66
|
+
delete env.ANTHROPIC_API_KEY; // force subscription auth, never credits
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await run("claude", cliArgs, { cwd: AGENT_HOME, env, maxBuffer: 32 * 1024 * 1024, timeout: 15 * 60 * 1000 });
|
|
69
|
+
const res = JSON.parse(stdout);
|
|
70
|
+
if (res.is_error) throw new Error(res.result || res.subtype || "agent errored");
|
|
71
|
+
usage.calls += 1;
|
|
72
|
+
usage.in += (res.usage?.input_tokens || 0) + (res.usage?.cache_creation_input_tokens || 0) + (res.usage?.cache_read_input_tokens || 0);
|
|
73
|
+
usage.out += res.usage?.output_tokens || 0;
|
|
74
|
+
usage.nominal += res.total_cost_usd || 0;
|
|
75
|
+
return res.result || "";
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Surface the REAL failure, not just "Command failed: <2000 chars of argv>"
|
|
78
|
+
const detail = [e.stderr, e.stdout].filter(Boolean).map((s) => String(s).slice(0, 400)).join(" | ");
|
|
79
|
+
const reason = detail || String(e.message || e).slice(0, 200);
|
|
80
|
+
if (attempt < 2) {
|
|
81
|
+
log("yellow", "retry", `agent call failed (${reason.slice(0, 120)}), retrying in 5s...`);
|
|
82
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
83
|
+
return claudeAgent({ prompt, system, model, tools }, attempt + 1);
|
|
84
|
+
}
|
|
85
|
+
throw new Error(reason);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Tolerant JSON extraction (headless output is prompted JSON, not schema-enforced).
|
|
90
|
+
function parseJson(text) {
|
|
91
|
+
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
92
|
+
const start = cleaned.indexOf("{");
|
|
93
|
+
const end = cleaned.lastIndexOf("}");
|
|
94
|
+
if (start === -1 || end === -1) throw new Error("no JSON object in agent output");
|
|
95
|
+
return JSON.parse(cleaned.slice(start, end + 1));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function pool(items, limit, fn) {
|
|
99
|
+
const results = new Array(items.length);
|
|
100
|
+
let next = 0;
|
|
101
|
+
async function runner() {
|
|
102
|
+
while (next < items.length) {
|
|
103
|
+
const i = next++;
|
|
104
|
+
results[i] = await fn(items[i], i).catch((e) => ({ error: String(e.message || e) }));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runner));
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------- phases ----------
|
|
112
|
+
async function plan(goal) {
|
|
113
|
+
const text = await claudeAgent({
|
|
114
|
+
model: CONDUCTOR_MODEL,
|
|
115
|
+
system: `You are the conductor of a multi-agent team. You never do the work yourself. You decompose the user's goal into at most ${MAX_AGENTS} INDEPENDENT tasks and assign each to a specialist (scout = finds facts, analyst = reasons and compares, writer = produces the deliverable). Tasks run in parallel and must not depend on each other. Every prompt must be fully self-contained.`,
|
|
116
|
+
prompt: `Goal: ${goal}\n\nReply with ONLY a JSON object, no prose, no code fences: {"strategy": "<one short paragraph>", "tasks": [{"role": "scout|analyst|writer", "title": "<max 6 words>", "prompt": "<complete self-contained prompt>"}]}`,
|
|
117
|
+
});
|
|
118
|
+
const p = parseJson(text);
|
|
119
|
+
p.tasks = (p.tasks || []).slice(0, MAX_AGENTS);
|
|
120
|
+
return p;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runWorker(task, i) {
|
|
124
|
+
const tools = USE_WEB && task.role === "scout" ? ["WebSearch", "WebFetch"] : [];
|
|
125
|
+
log("cyan", `agent ${i + 1}`, `${task.role}: ${task.title}${tools.length ? " (web on)" : ""}`);
|
|
126
|
+
const output = await claudeAgent({
|
|
127
|
+
model: WORKER_MODEL,
|
|
128
|
+
system: ROLES[task.role] || ROLES.analyst,
|
|
129
|
+
prompt: task.prompt,
|
|
130
|
+
tools,
|
|
131
|
+
});
|
|
132
|
+
log("green", `agent ${i + 1}`, `${task.title} done`);
|
|
133
|
+
return { ...task, output };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function runCritic(result, i) {
|
|
137
|
+
const text = await claudeAgent({
|
|
138
|
+
model: WORKER_MODEL,
|
|
139
|
+
system: ROLES.critic,
|
|
140
|
+
prompt: `The team's goal: ${GOAL}\n\nA ${result.role} agent was asked: ${result.prompt}\n\nIt produced:\n\n${result.output.slice(0, 12000)}\n\nAttack this work. Reply with ONLY a JSON object, no prose, no code fences: {"score": <1-10>, "verdict": "<one sentence>", "fixes": ["<fix>", ...]}`,
|
|
141
|
+
});
|
|
142
|
+
const crit = parseJson(text);
|
|
143
|
+
const color = crit.score >= 7 ? "green" : crit.score >= 5 ? "yellow" : "red";
|
|
144
|
+
log(color, `critic ${i + 1}`, `${result.title}: ${crit.score}/10, ${crit.verdict}`);
|
|
145
|
+
return crit;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function synthesize(goal, strategy, results) {
|
|
149
|
+
const dossier = results
|
|
150
|
+
.map((r, i) => `## Agent ${i + 1} (${r.role}): ${r.title}\nCritic verdict: ${r.crit ? `${r.crit.score}/10, ${r.crit.verdict} Fixes: ${(r.crit.fixes || []).join("; ") || "none"}` : "not critiqued"}\n\n${r.output}`)
|
|
151
|
+
.join("\n\n---\n\n");
|
|
152
|
+
return claudeAgent({
|
|
153
|
+
model: CONDUCTOR_MODEL,
|
|
154
|
+
system: ROLES.synthesizer,
|
|
155
|
+
prompt: `The goal: ${goal}\n\nThe strategy was: ${strategy}\n\nHere is everything the team produced, with critic verdicts:\n\n${dossier}\n\nProduce the final deliverable. Apply the critics' fixes. Drop anything scored under 5.`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------- main ----------
|
|
160
|
+
if (!GOAL) {
|
|
161
|
+
console.log('Usage: node orchestra-max.mjs "your goal" [--agents N] [--web] [--cheap] [--model M] [--worker M] [--no-critics]');
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(`\n${C.bold}ORCHESTRA MAX${C.reset} ${C.dim}subscription-powered · conductor=${CONDUCTOR_MODEL} workers=${WORKER_MODEL} max-agents=${MAX_AGENTS}${C.reset}\n`);
|
|
166
|
+
log("yellow", "goal", GOAL);
|
|
167
|
+
|
|
168
|
+
const t0 = Date.now();
|
|
169
|
+
log("cyan", "conductor", "planning...");
|
|
170
|
+
const p = await plan(GOAL);
|
|
171
|
+
log("green", "conductor", `strategy: ${p.strategy}`);
|
|
172
|
+
log("green", "conductor", `casting ${p.tasks.length} sub-agents`);
|
|
173
|
+
|
|
174
|
+
const slug = GOAL.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50).replace(/^-|-$/g, "");
|
|
175
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
176
|
+
const runFile = path.join(DIR, "runs", `${stamp}_${slug}.md`);
|
|
177
|
+
let ok = [];
|
|
178
|
+
let final = null;
|
|
179
|
+
let runError = null;
|
|
180
|
+
|
|
181
|
+
function saveRun() {
|
|
182
|
+
const header = `# ${GOAL}\n\n_${new Date().toISOString()} · ORCHESTRA MAX (subscription) · conductor ${CONDUCTOR_MODEL} · workers ${WORKER_MODEL} · ${usage.calls} calls · $0.00 credits (nominal value $${usage.nominal.toFixed(2)})_\n`;
|
|
183
|
+
const errNote = runError ? `\n> ⚠️ Run ended early: ${runError}\n` : "";
|
|
184
|
+
const finalSection = final ? `\n## Final deliverable\n\n${final}\n` : "";
|
|
185
|
+
const agents = ok.length
|
|
186
|
+
? `\n## Agent outputs\n\n${ok.map((r, i) => `### Agent ${i + 1} (${r.role}): ${r.title}\n${r.crit ? `_Critic: ${r.crit.score}/10, ${r.crit.verdict}_\n` : ""}\n${r.output}`).join("\n\n")}\n`
|
|
187
|
+
: "";
|
|
188
|
+
fs.writeFileSync(runFile, `${header}${errNote}${finalSection}\n---\n\n## Strategy\n\n${p.strategy}\n${agents}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const results = await pool(p.tasks, CONCURRENCY, runWorker);
|
|
193
|
+
ok = results.filter((r) => !r.error);
|
|
194
|
+
results.filter((r) => r.error).forEach((r) => log("red", "agent", `failed: ${r.error}`));
|
|
195
|
+
saveRun();
|
|
196
|
+
|
|
197
|
+
if (USE_CRITICS && ok.length) {
|
|
198
|
+
log("cyan", "critics", `attacking ${ok.length} results...`);
|
|
199
|
+
const crits = await pool(ok, CONCURRENCY, runCritic);
|
|
200
|
+
ok.forEach((r, i) => (r.crit = crits[i]?.error ? null : crits[i]));
|
|
201
|
+
saveRun();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
log("cyan", "conductor", "synthesizing final deliverable...\n");
|
|
205
|
+
console.log(`${C.dim}${"-".repeat(60)}${C.reset}`);
|
|
206
|
+
final = await synthesize(GOAL, p.strategy, ok);
|
|
207
|
+
console.log(final);
|
|
208
|
+
console.log(`${C.dim}${"-".repeat(60)}${C.reset}`);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
runError = String(e.message || e);
|
|
211
|
+
log("red", "error", runError);
|
|
212
|
+
}
|
|
213
|
+
saveRun();
|
|
214
|
+
|
|
215
|
+
const secs = ((Date.now() - t0) / 1000).toFixed(0);
|
|
216
|
+
const status = runError ? `${C.red}${C.bold}ENDED EARLY${C.reset}` : `${C.bold}DONE${C.reset}`;
|
|
217
|
+
console.log(`\n${status} ${usage.calls} agent calls · ${usage.in.toLocaleString()} in / ${usage.out.toLocaleString()} out tokens · ${C.bold}$0.00 API credits${C.reset} ${C.dim}(nominal value $${usage.nominal.toFixed(2)}, covered by your Max subscription)${C.reset} · ${secs}s`);
|
|
218
|
+
console.log(`${C.dim}saved: ${runFile}${runError ? " (partial)" : ""}${C.reset}\n`);
|
|
219
|
+
process.exit(runError ? 1 : 0);
|
package/orchestra.mjs
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ORCHESTRA — a multi-agent system in one file.
|
|
3
|
+
// One conductor agent takes your goal, breaks it into tasks, fans out specialist
|
|
4
|
+
// sub-agents in PARALLEL (scouts, analysts, writers), sends critics to attack
|
|
5
|
+
// their work, then synthesizes everything into one final deliverable.
|
|
6
|
+
//
|
|
7
|
+
// node orchestra.mjs "your goal here"
|
|
8
|
+
// node orchestra.mjs "your goal" --agents 4 --web
|
|
9
|
+
// node orchestra.mjs "your goal" --cheap (haiku everywhere, for testing)
|
|
10
|
+
// node orchestra.mjs "your goal" --model claude-fable-5
|
|
11
|
+
//
|
|
12
|
+
// Flags:
|
|
13
|
+
// --agents N max sub-agents the conductor may cast (default 5)
|
|
14
|
+
// --web let scout agents search the live web (server-side web search)
|
|
15
|
+
// --cheap use claude-haiku-4-5 for everything (pennies, for smoke tests)
|
|
16
|
+
// --model M model for conductor + synthesis (default claude-opus-4-8)
|
|
17
|
+
// --worker M model for sub-agents (default: same as --model)
|
|
18
|
+
// --no-critics skip the critic pass
|
|
19
|
+
//
|
|
20
|
+
// Auth: ANTHROPIC_API_KEY env var, or a .env file next to this script.
|
|
21
|
+
|
|
22
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { ROLES } from "./roles.mjs";
|
|
27
|
+
|
|
28
|
+
const DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
// ---------- config ----------
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const flag = (name) => {
|
|
33
|
+
const i = args.indexOf(name);
|
|
34
|
+
return i === -1 ? null : true;
|
|
35
|
+
};
|
|
36
|
+
const opt = (name, dflt) => {
|
|
37
|
+
const i = args.indexOf(name);
|
|
38
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : dflt;
|
|
39
|
+
};
|
|
40
|
+
const GOAL = args.filter((a, i) => !a.startsWith("--") && (i === 0 || args[i - 1] !== "--agents" && !["--model", "--worker"].includes(args[i - 1]))).join(" ").trim();
|
|
41
|
+
|
|
42
|
+
const CHEAP = flag("--cheap");
|
|
43
|
+
const CONDUCTOR_MODEL = CHEAP ? "claude-haiku-4-5" : opt("--model", "claude-opus-4-8");
|
|
44
|
+
const WORKER_MODEL = CHEAP ? "claude-haiku-4-5" : opt("--worker", opt("--model", "claude-opus-4-8"));
|
|
45
|
+
const MAX_AGENTS = parseInt(opt("--agents", "5"), 10);
|
|
46
|
+
const USE_WEB = flag("--web");
|
|
47
|
+
const USE_CRITICS = !flag("--no-critics");
|
|
48
|
+
const CONCURRENCY = 4;
|
|
49
|
+
|
|
50
|
+
// $/MTok input, output — for the cost meter
|
|
51
|
+
const PRICES = {
|
|
52
|
+
"claude-fable-5": [10, 50],
|
|
53
|
+
"claude-opus-4-8": [5, 25],
|
|
54
|
+
"claude-opus-4-7": [5, 25],
|
|
55
|
+
"claude-opus-4-6": [5, 25],
|
|
56
|
+
"claude-sonnet-4-6": [3, 15],
|
|
57
|
+
"claude-haiku-4-5": [1, 5],
|
|
58
|
+
};
|
|
59
|
+
const supportsAdaptive = (m) => /fable-5|opus-4-[678]|sonnet-4-6/.test(m);
|
|
60
|
+
const supportsWebSearch = (m) => /fable-5|opus-4-[678]|sonnet-4-6/.test(m);
|
|
61
|
+
|
|
62
|
+
// ---------- auth ----------
|
|
63
|
+
function loadApiKey() {
|
|
64
|
+
if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY;
|
|
65
|
+
const envFile = path.join(DIR, ".env");
|
|
66
|
+
if (fs.existsSync(envFile)) {
|
|
67
|
+
const m = fs.readFileSync(envFile, "utf8").match(/ANTHROPIC_API_KEY\s*=\s*(\S+)/);
|
|
68
|
+
if (m) return m[1];
|
|
69
|
+
}
|
|
70
|
+
console.error("No API key. Set ANTHROPIC_API_KEY or create a .env file next to this script.");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const client = new Anthropic({ apiKey: loadApiKey() });
|
|
74
|
+
|
|
75
|
+
// ---------- helpers ----------
|
|
76
|
+
const C = { dim: "\x1b[2m", cyan: "\x1b[36m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", bold: "\x1b[1m", reset: "\x1b[0m" };
|
|
77
|
+
const log = (color, tag, msg) => console.log(`${C[color]}${C.bold}[${tag}]${C.reset} ${msg}`);
|
|
78
|
+
|
|
79
|
+
const usage = { calls: 0, in: 0, out: 0, cost: 0 };
|
|
80
|
+
function track(model, u) {
|
|
81
|
+
const [pin, pout] = PRICES[model] || [5, 25];
|
|
82
|
+
usage.calls += 1;
|
|
83
|
+
usage.in += u.input_tokens + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
84
|
+
usage.out += u.output_tokens;
|
|
85
|
+
usage.cost += (u.input_tokens / 1e6) * pin + (u.output_tokens / 1e6) * pout + ((u.cache_creation_input_tokens || 0) / 1e6) * pin * 1.25 + ((u.cache_read_input_tokens || 0) / 1e6) * pin * 0.1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function textOf(msg) {
|
|
89
|
+
if (msg.stop_reason === "refusal") return "[This agent's request was declined by safety classifiers.]";
|
|
90
|
+
return msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function pool(items, limit, fn) {
|
|
94
|
+
const results = new Array(items.length);
|
|
95
|
+
let next = 0;
|
|
96
|
+
async function runner() {
|
|
97
|
+
while (next < items.length) {
|
|
98
|
+
const i = next++;
|
|
99
|
+
results[i] = await fn(items[i], i).catch((e) => ({ error: String(e.message || e) }));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runner));
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const maybeThinking = (model) => (supportsAdaptive(model) ? { thinking: { type: "adaptive" } } : {});
|
|
107
|
+
|
|
108
|
+
// ---------- phase 1: PLAN (conductor, structured output) ----------
|
|
109
|
+
const PLAN_SCHEMA = {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
strategy: { type: "string", description: "One short paragraph: how the team will attack this goal." },
|
|
113
|
+
tasks: {
|
|
114
|
+
type: "array",
|
|
115
|
+
description: "Independent tasks, one per sub-agent. Each must be self-contained (the sub-agent sees nothing else).",
|
|
116
|
+
items: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
role: { type: "string", enum: ["scout", "analyst", "writer"] },
|
|
120
|
+
title: { type: "string", description: "Short label for progress display, max 6 words." },
|
|
121
|
+
prompt: { type: "string", description: "The complete prompt for this sub-agent, fully self-contained, including the goal context it needs." },
|
|
122
|
+
},
|
|
123
|
+
required: ["role", "title", "prompt"],
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: ["strategy", "tasks"],
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
async function plan(goal) {
|
|
133
|
+
const res = await client.messages.create({
|
|
134
|
+
model: CONDUCTOR_MODEL,
|
|
135
|
+
max_tokens: 8000,
|
|
136
|
+
...maybeThinking(CONDUCTOR_MODEL),
|
|
137
|
+
system: `You are the conductor of a multi-agent team. You never do the work yourself. You decompose the user's goal into at most ${MAX_AGENTS} INDEPENDENT tasks and assign each to a specialist sub-agent (scout = finds facts, analyst = reasons and compares, writer = produces the deliverable). Tasks run in parallel, so they must not depend on each other. Prefer fewer, sharper tasks over many vague ones. Every prompt you write must be fully self-contained: the sub-agent sees only its own prompt.`,
|
|
138
|
+
messages: [{ role: "user", content: `Goal: ${goal}` }],
|
|
139
|
+
output_config: { format: { type: "json_schema", schema: PLAN_SCHEMA } },
|
|
140
|
+
});
|
|
141
|
+
track(CONDUCTOR_MODEL, res.usage);
|
|
142
|
+
const parsed = JSON.parse(textOf(res));
|
|
143
|
+
parsed.tasks = parsed.tasks.slice(0, MAX_AGENTS);
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------- phase 2: EXECUTE (sub-agents, parallel) ----------
|
|
148
|
+
async function runWorker(task, i) {
|
|
149
|
+
const tools = [];
|
|
150
|
+
if (USE_WEB && task.role === "scout" && supportsWebSearch(WORKER_MODEL)) {
|
|
151
|
+
tools.push({ type: "web_search_20260209", name: "web_search", max_uses: 3 });
|
|
152
|
+
}
|
|
153
|
+
log("cyan", `agent ${i + 1}`, `${task.role}: ${task.title} ${tools.length ? "(web search on)" : ""}`);
|
|
154
|
+
const stream = client.messages.stream({
|
|
155
|
+
model: WORKER_MODEL,
|
|
156
|
+
max_tokens: 6000,
|
|
157
|
+
system: ROLES[task.role] || ROLES.analyst,
|
|
158
|
+
messages: [{ role: "user", content: task.prompt }],
|
|
159
|
+
...(tools.length ? { tools } : {}),
|
|
160
|
+
});
|
|
161
|
+
const msg = await stream.finalMessage();
|
|
162
|
+
track(WORKER_MODEL, msg.usage);
|
|
163
|
+
log("green", `agent ${i + 1}`, `${task.title} done (${msg.usage.output_tokens} tokens)`);
|
|
164
|
+
return { ...task, output: textOf(msg) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------- phase 3: CRITIQUE (one critic per result, parallel) ----------
|
|
168
|
+
const CRIT_SCHEMA = {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
score: { type: "integer", description: "1 (garbage) to 10 (ship it)" },
|
|
172
|
+
verdict: { type: "string", description: "One sentence: the biggest problem, or why it holds up." },
|
|
173
|
+
fixes: { type: "array", items: { type: "string" }, description: "Concrete fixes, empty if none needed." },
|
|
174
|
+
},
|
|
175
|
+
required: ["score", "verdict", "fixes"],
|
|
176
|
+
additionalProperties: false,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
async function runCritic(result, i) {
|
|
180
|
+
const res = await client.messages.create({
|
|
181
|
+
model: WORKER_MODEL,
|
|
182
|
+
max_tokens: 2000,
|
|
183
|
+
system: ROLES.critic,
|
|
184
|
+
messages: [{ role: "user", content: `The team's goal: ${GOAL}\n\nA ${result.role} agent was asked: ${result.prompt}\n\nIt produced:\n\n${result.output.slice(0, 12000)}\n\nAttack this work. Return your structured verdict.` }],
|
|
185
|
+
output_config: { format: { type: "json_schema", schema: CRIT_SCHEMA } },
|
|
186
|
+
});
|
|
187
|
+
track(WORKER_MODEL, res.usage);
|
|
188
|
+
const crit = JSON.parse(textOf(res));
|
|
189
|
+
const color = crit.score >= 7 ? "green" : crit.score >= 5 ? "yellow" : "red";
|
|
190
|
+
log(color, `critic ${i + 1}`, `${result.title}: ${crit.score}/10, ${crit.verdict}`);
|
|
191
|
+
return crit;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------- phase 4: SYNTHESIZE (conductor, streamed live) ----------
|
|
195
|
+
async function synthesize(goal, strategy, results) {
|
|
196
|
+
const dossier = results
|
|
197
|
+
.map((r, i) => `## Agent ${i + 1} (${r.role}): ${r.title}\nCritic verdict: ${r.crit ? `${r.crit.score}/10, ${r.crit.verdict} Fixes: ${r.crit.fixes.join("; ") || "none"}` : "not critiqued"}\n\n${r.output}`)
|
|
198
|
+
.join("\n\n---\n\n");
|
|
199
|
+
const stream = client.messages.stream({
|
|
200
|
+
model: CONDUCTOR_MODEL,
|
|
201
|
+
max_tokens: 16000,
|
|
202
|
+
...maybeThinking(CONDUCTOR_MODEL),
|
|
203
|
+
system: ROLES.synthesizer,
|
|
204
|
+
messages: [{ role: "user", content: `The goal: ${goal}\n\nThe strategy was: ${strategy}\n\nHere is everything the team produced, with critic verdicts:\n\n${dossier}\n\nProduce the final deliverable. Apply the critics' fixes. Drop anything scored under 5.` }],
|
|
205
|
+
});
|
|
206
|
+
stream.on("text", (t) => process.stdout.write(t));
|
|
207
|
+
const msg = await stream.finalMessage();
|
|
208
|
+
track(CONDUCTOR_MODEL, msg.usage);
|
|
209
|
+
return textOf(msg);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------- main ----------
|
|
213
|
+
if (!GOAL) {
|
|
214
|
+
console.log('Usage: node orchestra.mjs "your goal" [--agents N] [--web] [--cheap] [--model M] [--worker M] [--no-critics]');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log(`\n${C.bold}ORCHESTRA${C.reset} ${C.dim}conductor=${CONDUCTOR_MODEL} workers=${WORKER_MODEL} max-agents=${MAX_AGENTS}${C.reset}\n`);
|
|
219
|
+
log("yellow", "goal", GOAL);
|
|
220
|
+
|
|
221
|
+
const t0 = Date.now();
|
|
222
|
+
log("cyan", "conductor", "planning...");
|
|
223
|
+
const p = await plan(GOAL);
|
|
224
|
+
log("green", "conductor", `strategy: ${p.strategy}`);
|
|
225
|
+
log("green", "conductor", `casting ${p.tasks.length} sub-agents`);
|
|
226
|
+
|
|
227
|
+
// Every phase saves to the run file as it completes, so a mid-run failure
|
|
228
|
+
// (rate limit, empty credit balance, network drop) never loses paid-for work.
|
|
229
|
+
const slug = GOAL.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50).replace(/^-|-$/g, "");
|
|
230
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
231
|
+
const runFile = path.join(DIR, "runs", `${stamp}_${slug}.md`);
|
|
232
|
+
let ok = [];
|
|
233
|
+
let final = null;
|
|
234
|
+
let runError = null;
|
|
235
|
+
|
|
236
|
+
function saveRun() {
|
|
237
|
+
const header = `# ${GOAL}\n\n_${new Date().toISOString()} · conductor ${CONDUCTOR_MODEL} · workers ${WORKER_MODEL} · ${usage.calls} calls · $${usage.cost.toFixed(4)}_\n`;
|
|
238
|
+
const errNote = runError ? `\n> ⚠️ Run ended early: ${runError}\n` : "";
|
|
239
|
+
const finalSection = final ? `\n## Final deliverable\n\n${final}\n` : "";
|
|
240
|
+
const agents = ok.length
|
|
241
|
+
? `\n## Agent outputs\n\n${ok.map((r, i) => `### Agent ${i + 1} (${r.role}): ${r.title}\n${r.crit ? `_Critic: ${r.crit.score}/10, ${r.crit.verdict}_\n` : ""}\n${r.output}`).join("\n\n")}\n`
|
|
242
|
+
: "";
|
|
243
|
+
fs.writeFileSync(runFile, `${header}${errNote}${finalSection}\n---\n\n## Strategy\n\n${p.strategy}\n${agents}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const results = await pool(p.tasks, CONCURRENCY, runWorker);
|
|
248
|
+
ok = results.filter((r) => !r.error);
|
|
249
|
+
results.filter((r) => r.error).forEach((r) => log("red", "agent", `failed: ${r.error}`));
|
|
250
|
+
saveRun(); // worker outputs are now on disk no matter what happens next
|
|
251
|
+
|
|
252
|
+
if (USE_CRITICS && ok.length) {
|
|
253
|
+
log("cyan", "critics", `attacking ${ok.length} results...`);
|
|
254
|
+
const crits = await pool(ok, CONCURRENCY, runCritic);
|
|
255
|
+
ok.forEach((r, i) => (r.crit = crits[i]?.error ? null : crits[i]));
|
|
256
|
+
saveRun();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
log("cyan", "conductor", "synthesizing final deliverable...\n");
|
|
260
|
+
console.log(`${C.dim}${"-".repeat(60)}${C.reset}`);
|
|
261
|
+
final = await synthesize(GOAL, p.strategy, ok);
|
|
262
|
+
console.log(`\n${C.dim}${"-".repeat(60)}${C.reset}`);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
runError = String(e.message || e);
|
|
265
|
+
if (/credit balance is too low/i.test(runError)) {
|
|
266
|
+
log("red", "billing", "Anthropic API credit balance is empty. Top up at console.anthropic.com > Plans & Billing, then re-run.");
|
|
267
|
+
} else {
|
|
268
|
+
log("red", "error", runError);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
saveRun();
|
|
272
|
+
|
|
273
|
+
const secs = ((Date.now() - t0) / 1000).toFixed(0);
|
|
274
|
+
const status = runError ? `${C.red}${C.bold}ENDED EARLY${C.reset}` : `${C.bold}DONE${C.reset}`;
|
|
275
|
+
console.log(`\n${status} ${usage.calls} agent calls · ${usage.in.toLocaleString()} in / ${usage.out.toLocaleString()} out tokens · ${C.bold}$${usage.cost.toFixed(4)}${C.reset} · ${secs}s`);
|
|
276
|
+
console.log(`${C.dim}saved: ${runFile}${runError ? " (partial: worker outputs preserved)" : ""}${C.reset}\n`);
|
|
277
|
+
process.exit(runError ? 1 : 0);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orchestra-agents",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-agent orchestrator on the Claude API: one conductor plans, sub-agents execute in parallel, adversarial critics attack, the conductor synthesizes one deliverable.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"orchestra": "./orchestra.mjs",
|
|
8
|
+
"orchestra-max": "./orchestra-max.mjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node orchestra.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"orchestra.mjs",
|
|
15
|
+
"orchestra-max.mjs",
|
|
16
|
+
"roles.mjs",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"anthropic",
|
|
23
|
+
"agents",
|
|
24
|
+
"multi-agent",
|
|
25
|
+
"orchestrator",
|
|
26
|
+
"swarm",
|
|
27
|
+
"ai"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/vyn-store/orchestra-agents.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "Kirk Kodre",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@anthropic-ai/sdk": "^0.109.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/roles.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Sub-agent role definitions. Each worker gets ONE of these as its system prompt.
|
|
2
|
+
// The conductor decides which roles to cast for each task.
|
|
3
|
+
|
|
4
|
+
export const ROLES = {
|
|
5
|
+
scout: `You are a research scout in a multi-agent team. Your only job is to FIND things.
|
|
6
|
+
Return concrete findings: facts, numbers, names, sources. No opinions, no recommendations, no fluff.
|
|
7
|
+
If you can search the web, cite where each fact came from. If you cannot verify something, say so explicitly.
|
|
8
|
+
Your output goes to an orchestrator that will synthesize it with other agents' work, so return raw, dense findings, not a polished essay.`,
|
|
9
|
+
|
|
10
|
+
analyst: `You are an analyst in a multi-agent team. Your job is to REASON: compare options, run the numbers, find the tradeoffs, stress-test assumptions.
|
|
11
|
+
Show your working. Be specific and quantitative wherever possible.
|
|
12
|
+
Your output goes to an orchestrator that will synthesize it with other agents' work, so lead with your conclusions, then the reasoning behind them.`,
|
|
13
|
+
|
|
14
|
+
writer: `You are a writer in a multi-agent team. Your job is to PRODUCE the actual deliverable: copy, script, plan, document.
|
|
15
|
+
Write tight and concrete. No em-dashes (use colons, periods, commas). No filler phrases.
|
|
16
|
+
Your output goes to an orchestrator and then a critic, so produce the best complete draft you can, not an outline.
|
|
17
|
+
Start directly with the deliverable. Never mention your tools, skills, process, or that you are an AI agent.`,
|
|
18
|
+
|
|
19
|
+
critic: `You are an adversarial critic in a multi-agent team. Your job is to try to BREAK the work you are given.
|
|
20
|
+
Assume it is wrong until proven right. Find factual errors, weak reasoning, missing pieces, and anything that would embarrass the team if shipped.
|
|
21
|
+
Be specific: point at the exact sentence or claim that fails and say why. If the work genuinely holds up, say so, but you must earn that verdict.`,
|
|
22
|
+
|
|
23
|
+
synthesizer: `You are the synthesizer in a multi-agent team. You receive the outputs of several specialist agents plus critic verdicts.
|
|
24
|
+
Merge them into ONE coherent final deliverable. Resolve contradictions. Drop anything the critics killed. Keep the strongest material.
|
|
25
|
+
Write the final answer directly for the human who asked, complete and ready to use. No meta-commentary about the agents or the process.`,
|
|
26
|
+
};
|