lazyclaw 3.99.20 → 3.99.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/cli.mjs +12 -0
- package/package.json +1 -1
- package/providers/orchestrator.mjs +273 -0
- package/providers/registry.mjs +35 -0
package/README.md
CHANGED
|
@@ -81,6 +81,36 @@ lazyclaw onboard --non-interactive --provider nim \
|
|
|
81
81
|
|
|
82
82
|
Need a vendor that's **not** built-in? `+ Add a custom OpenAI-compatible endpoint…` inside the setup picker (or `lazyclaw providers add <name> --base-url <url>`) still works for vLLM / LM Studio / private gateways / anything else that speaks the OpenAI v1 wire format.
|
|
83
83
|
|
|
84
|
+
### `orchestrator` — multi-agent dispatch as a provider
|
|
85
|
+
|
|
86
|
+
`orchestrator` is a synthetic provider that composes the others. A chat message hitting `PROVIDERS.orchestrator` triggers a three-phase pipeline instead of a single 1:1 call:
|
|
87
|
+
|
|
88
|
+
1. **PLAN** — the *planner* provider decomposes the request into 2–5 parallel subtasks (JSON-only system prompt; fences / prose tolerated).
|
|
89
|
+
2. **EXECUTE** — each subtask is dispatched round-robin across the *workers*. Replies stream inline so you watch progress in real time.
|
|
90
|
+
3. **SYNTHESIS** — the planner re-enters with every worker's output and writes the final user-facing answer.
|
|
91
|
+
|
|
92
|
+
Configure in `~/.lazyclaw/config.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"provider": "orchestrator",
|
|
97
|
+
"orchestrator": {
|
|
98
|
+
"planner": "claude-cli:claude-opus-4-7",
|
|
99
|
+
"workers": [
|
|
100
|
+
"claude-cli:claude-sonnet-4-6",
|
|
101
|
+
"openai:gpt-4o",
|
|
102
|
+
"gemini:gemini-2.5-pro",
|
|
103
|
+
"nim:meta/llama-3.1-405b-instruct"
|
|
104
|
+
],
|
|
105
|
+
"maxSubtasks": 5
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Then `lazyclaw chat` (or any other entry point that ends up calling a provider — `lazyclaw agent`, the daemon's `POST /agent` / `POST /chat`, the dashboard chat tab) routes through the orchestrator. Each worker's api-key is resolved through the same chain a direct chat would use (`authProfiles` → `customProviders` → built-in env var → legacy `cfg['api-key']`).
|
|
111
|
+
|
|
112
|
+
Defaults fall back gracefully: `planner` defaults to `cfg.provider`/`cfg.model`, `workers` defaults to `[planner]` (single-agent chain, still benefits from plan + synthesis structure). Self-recursion (`planner: "orchestrator"`) is rejected up front.
|
|
113
|
+
|
|
84
114
|
## Launcher (no-arg `lazyclaw`)
|
|
85
115
|
|
|
86
116
|
Running `lazyclaw` with no subcommand drops into an arrow-key launcher with every subcommand laid out as a menu. Navigation:
|
package/cli.mjs
CHANGED
|
@@ -707,6 +707,18 @@ async function ensureRegistry() {
|
|
|
707
707
|
_registryMod.registerCustomProviders(readConfig());
|
|
708
708
|
}
|
|
709
709
|
} catch { /* never let a malformed cfg.customProviders block startup */ }
|
|
710
|
+
// Wire the orchestrator's live cfg + auth-key resolver. We do this on
|
|
711
|
+
// every ensureRegistry() call (cheap — just replaces the closure) so a
|
|
712
|
+
// mid-session config edit (custom provider added, env var exported)
|
|
713
|
+
// takes effect on the next orchestrator turn without a restart.
|
|
714
|
+
try {
|
|
715
|
+
if (typeof _registryMod.registerOrchestrator === 'function') {
|
|
716
|
+
_registryMod.registerOrchestrator({
|
|
717
|
+
cfgGetter: readConfig,
|
|
718
|
+
keyResolver: _resolveAuthKey,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
} catch { /* defensive */ }
|
|
710
722
|
return _registryMod;
|
|
711
723
|
}
|
|
712
724
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "3.99.
|
|
3
|
+
"version": "3.99.21",
|
|
4
4
|
"description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama and orchestrating multi-step LLM workflows. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Orchestrator provider — "openclaw-style" multi-agent dispatch.
|
|
2
|
+
//
|
|
3
|
+
// A user message arriving at PROVIDERS.orchestrator is NOT forwarded
|
|
4
|
+
// 1:1 to a single backend. Instead the provider performs three phases:
|
|
5
|
+
//
|
|
6
|
+
// 1. PLAN — the configured planner provider decomposes the task
|
|
7
|
+
// into 2–5 self-contained subtasks (JSON shape).
|
|
8
|
+
// 2. EXECUTE — each subtask is dispatched to a worker provider
|
|
9
|
+
// (round-robin over cfg.orchestrator.workers). Workers
|
|
10
|
+
// stream their replies; the orchestrator surfaces them
|
|
11
|
+
// inline so the user can watch progress.
|
|
12
|
+
// 3. SYNTHESIS — the planner re-enters with all subtask outputs and
|
|
13
|
+
// produces the final answer.
|
|
14
|
+
//
|
|
15
|
+
// Provider/model spec is "<provider>:<model>" (same shape as the chat
|
|
16
|
+
// REPL's `/model anthropic/claude-opus-4-7` after normalisation). When
|
|
17
|
+
// the model part is omitted, the worker's defaultModel from
|
|
18
|
+
// PROVIDER_INFO is used.
|
|
19
|
+
//
|
|
20
|
+
// Config (~/.lazyclaw/config.json):
|
|
21
|
+
// {
|
|
22
|
+
// "orchestrator": {
|
|
23
|
+
// "planner": "claude-cli:claude-opus-4-7",
|
|
24
|
+
// "workers": [
|
|
25
|
+
// "claude-cli:claude-sonnet-4-6",
|
|
26
|
+
// "openai:gpt-4o",
|
|
27
|
+
// "gemini:gemini-2.5-pro"
|
|
28
|
+
// ],
|
|
29
|
+
// "maxSubtasks": 5, // optional, default 5
|
|
30
|
+
// "concurrency": 0 // optional, 0 = sequential (visible streaming)
|
|
31
|
+
// }
|
|
32
|
+
// }
|
|
33
|
+
//
|
|
34
|
+
// Defaults: planner = the user's currently configured `cfg.provider`
|
|
35
|
+
// (so `lazyclaw onboard --provider claude-cli` works without any extra
|
|
36
|
+
// step), workers = [planner] (degenerates to a single-agent chain that
|
|
37
|
+
// still benefits from plan + synthesis structure).
|
|
38
|
+
|
|
39
|
+
import { PROVIDERS, PROVIDER_INFO } from './registry.mjs';
|
|
40
|
+
|
|
41
|
+
function _parseSpec(spec) {
|
|
42
|
+
if (!spec || typeof spec !== 'string') return { provider: '', model: '' };
|
|
43
|
+
const colon = spec.indexOf(':');
|
|
44
|
+
if (colon < 0) return { provider: spec.trim(), model: '' };
|
|
45
|
+
return { provider: spec.slice(0, colon).trim(), model: spec.slice(colon + 1).trim() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _lookupProvider(spec) {
|
|
49
|
+
const { provider, model } = _parseSpec(spec);
|
|
50
|
+
const prov = PROVIDERS[provider];
|
|
51
|
+
if (!prov) return null;
|
|
52
|
+
const info = PROVIDER_INFO[provider] || {};
|
|
53
|
+
return {
|
|
54
|
+
name: provider,
|
|
55
|
+
model: model || info.defaultModel || '',
|
|
56
|
+
prov,
|
|
57
|
+
info,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _bestPlanArray(text) {
|
|
62
|
+
// Planners sometimes wrap the JSON in prose / code fences. Try the
|
|
63
|
+
// raw response first, then the largest [...] / [...]-shaped span.
|
|
64
|
+
const tryParse = (s) => {
|
|
65
|
+
try { return JSON.parse(s); } catch { return null; }
|
|
66
|
+
};
|
|
67
|
+
let arr = tryParse(text);
|
|
68
|
+
if (Array.isArray(arr)) return arr;
|
|
69
|
+
// Strip ```json fences
|
|
70
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]+?)```/);
|
|
71
|
+
if (fence) {
|
|
72
|
+
arr = tryParse(fence[1].trim());
|
|
73
|
+
if (Array.isArray(arr)) return arr;
|
|
74
|
+
}
|
|
75
|
+
// Largest [...] substring
|
|
76
|
+
const start = text.indexOf('[');
|
|
77
|
+
const end = text.lastIndexOf(']');
|
|
78
|
+
if (start >= 0 && end > start) {
|
|
79
|
+
arr = tryParse(text.slice(start, end + 1));
|
|
80
|
+
if (Array.isArray(arr)) return arr;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const PLANNER_SYSTEM = `You are an orchestrator that decomposes a user request into independent subtasks for parallel worker agents.
|
|
86
|
+
|
|
87
|
+
Rules:
|
|
88
|
+
- Output ONLY a JSON array. No prose, no markdown, no code fences.
|
|
89
|
+
- Each entry has shape { "id": <int>, "task": "<one-sentence imperative>", "rationale": "<why this is a useful slice>" }.
|
|
90
|
+
- 2 to 5 subtasks. Each must be doable WITHOUT seeing the others' outputs (parallel-safe).
|
|
91
|
+
- If the request is genuinely atomic (e.g. "say hi"), return a single-element array.
|
|
92
|
+
- Do not add a synthesis / merge step — that runs separately after workers complete.
|
|
93
|
+
- Subtasks must be self-contained: include any context a worker needs to act on the task alone.`;
|
|
94
|
+
|
|
95
|
+
const SYNTHESIS_SYSTEM = `You are an orchestrator producing the final answer for the user.
|
|
96
|
+
|
|
97
|
+
You receive: (1) the user's original request, (2) the subtask plan you produced, (3) each worker's response.
|
|
98
|
+
|
|
99
|
+
Rules:
|
|
100
|
+
- Synthesize a single coherent answer. Distill — do not echo each worker verbatim.
|
|
101
|
+
- Cite worker findings briefly when they meaningfully diverge ("Worker A found …, Worker B confirmed").
|
|
102
|
+
- If a worker failed, acknowledge it but do not let it block the rest of the answer.
|
|
103
|
+
- Match the tone and length the user implied (one-line question → one-line answer; deep dive → deep dive).
|
|
104
|
+
- No JSON; this is the human-facing reply.`;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build an orchestrator provider. The chat REPL / agent / daemon path
|
|
108
|
+
* treats it like any other provider — the `sendMessage` async iterable
|
|
109
|
+
* yields markdown chunks describing plan + subtasks + synthesis.
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} [opts]
|
|
112
|
+
* @param {() => Record<string, unknown>} [opts.cfgGetter] reads ~/.lazyclaw/config.json
|
|
113
|
+
* @param {(cfg, provider) => string} [opts.keyResolver] returns api-key for a worker provider (mirrors cli.mjs::_resolveAuthKey)
|
|
114
|
+
*/
|
|
115
|
+
export function makeOrchestratorProvider(opts = {}) {
|
|
116
|
+
const cfgGetter = typeof opts.cfgGetter === 'function' ? opts.cfgGetter : () => ({});
|
|
117
|
+
const keyResolver = typeof opts.keyResolver === 'function' ? opts.keyResolver : () => '';
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
name: 'orchestrator',
|
|
121
|
+
async *sendMessage(messages, callerOpts = {}) {
|
|
122
|
+
const cfg = cfgGetter() || {};
|
|
123
|
+
const o = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
|
|
124
|
+
const fallbackSpec = cfg.provider && cfg.provider !== 'orchestrator'
|
|
125
|
+
? `${cfg.provider}${cfg.model ? ':' + cfg.model : ''}`
|
|
126
|
+
: 'claude-cli';
|
|
127
|
+
const plannerSpec = String(o.planner || fallbackSpec);
|
|
128
|
+
const workerSpecs = Array.isArray(o.workers) && o.workers.length
|
|
129
|
+
? o.workers.map(String)
|
|
130
|
+
: [plannerSpec];
|
|
131
|
+
const maxSubtasks = Number.isFinite(o.maxSubtasks) && o.maxSubtasks > 0 ? Math.min(10, o.maxSubtasks) : 5;
|
|
132
|
+
|
|
133
|
+
const planner = _lookupProvider(plannerSpec);
|
|
134
|
+
if (!planner) {
|
|
135
|
+
yield `⚠ orchestrator: planner provider "${plannerSpec}" is not registered. ` +
|
|
136
|
+
`Set cfg.orchestrator.planner to a valid "provider:model" (e.g. "claude-cli:claude-opus-4-7").\n`;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Self-recursion guard: a misconfigured cfg.orchestrator.planner =
|
|
140
|
+
// "orchestrator" would otherwise spin forever, with each call
|
|
141
|
+
// dispatching back to itself.
|
|
142
|
+
if (planner.name === 'orchestrator') {
|
|
143
|
+
yield `⚠ orchestrator: planner cannot be "orchestrator" — set cfg.orchestrator.planner to a real provider (e.g. "claude-cli:claude-opus-4-7").\n`;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const workers = workerSpecs.map(_lookupProvider).filter(Boolean).filter(w => w.name !== 'orchestrator');
|
|
147
|
+
if (workers.length === 0) {
|
|
148
|
+
yield `⚠ orchestrator: no usable workers (cfg.orchestrator.workers is empty, all unknown, or only references "orchestrator" itself).\n`;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const userText = (() => {
|
|
153
|
+
// Most recent user message becomes the orchestration target. We
|
|
154
|
+
// pass earlier turns as context to the planner only — workers
|
|
155
|
+
// see a self-contained subtask string, not chat history.
|
|
156
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
157
|
+
if (messages[i].role === 'user') return String(messages[i].content || '');
|
|
158
|
+
}
|
|
159
|
+
return '';
|
|
160
|
+
})();
|
|
161
|
+
|
|
162
|
+
// ── Phase 1: PLAN ───────────────────────────────────────────────
|
|
163
|
+
yield `## 🦞 Orchestrator\n\n`;
|
|
164
|
+
yield `Planner: \`${planner.name}${planner.model ? ':' + planner.model : ''}\` · Workers: ${workers.map(w => `\`${w.name}${w.model ? ':' + w.model : ''}\``).join(', ')}\n\n`;
|
|
165
|
+
yield `### 1. Planning\n\n`;
|
|
166
|
+
|
|
167
|
+
const plannerMessages = [
|
|
168
|
+
{ role: 'system', content: PLANNER_SYSTEM },
|
|
169
|
+
...messages.filter(m => m.role === 'user' || m.role === 'assistant'),
|
|
170
|
+
];
|
|
171
|
+
let planRaw = '';
|
|
172
|
+
try {
|
|
173
|
+
for await (const chunk of planner.prov.sendMessage(plannerMessages, {
|
|
174
|
+
apiKey: keyResolver(cfg, planner.name),
|
|
175
|
+
model: planner.model || undefined,
|
|
176
|
+
signal: callerOpts.signal,
|
|
177
|
+
maxTokens: 1024,
|
|
178
|
+
})) {
|
|
179
|
+
planRaw += String(chunk);
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
yield `⚠ planner error: ${e?.message || String(e)}\n\n`;
|
|
183
|
+
// Fallback: hand the user message to the first worker directly.
|
|
184
|
+
const w = workers[0];
|
|
185
|
+
yield `Falling back to direct call on \`${w.name}${w.model ? ':' + w.model : ''}\`:\n\n`;
|
|
186
|
+
for await (const chunk of w.prov.sendMessage(messages, {
|
|
187
|
+
apiKey: keyResolver(cfg, w.name),
|
|
188
|
+
model: w.model || undefined,
|
|
189
|
+
signal: callerOpts.signal,
|
|
190
|
+
})) yield String(chunk);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const plan = _bestPlanArray(planRaw);
|
|
195
|
+
if (!plan || plan.length === 0) {
|
|
196
|
+
yield `⚠ planner returned no parseable JSON plan. Raw output:\n\n\`\`\`\n${planRaw.trim().slice(0, 800)}\n\`\`\`\n\nFalling back to single-shot on \`${planner.name}${planner.model ? ':' + planner.model : ''}\`:\n\n`;
|
|
197
|
+
for await (const chunk of planner.prov.sendMessage(messages, {
|
|
198
|
+
apiKey: keyResolver(cfg, planner.name),
|
|
199
|
+
model: planner.model || undefined,
|
|
200
|
+
signal: callerOpts.signal,
|
|
201
|
+
})) yield String(chunk);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const trimmed = plan.slice(0, maxSubtasks).map((p, i) => ({
|
|
205
|
+
id: Number.isFinite(p?.id) ? p.id : i + 1,
|
|
206
|
+
task: String(p?.task || '').trim(),
|
|
207
|
+
rationale: String(p?.rationale || '').trim(),
|
|
208
|
+
})).filter(p => p.task);
|
|
209
|
+
if (trimmed.length === 0) {
|
|
210
|
+
yield `⚠ plan parsed but contained no usable subtasks. Falling back.\n\n`;
|
|
211
|
+
for await (const chunk of planner.prov.sendMessage(messages, {
|
|
212
|
+
apiKey: keyResolver(cfg, planner.name),
|
|
213
|
+
model: planner.model || undefined,
|
|
214
|
+
signal: callerOpts.signal,
|
|
215
|
+
})) yield String(chunk);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const p of trimmed) {
|
|
220
|
+
yield `${p.id}. **${p.task}**${p.rationale ? ` _— ${p.rationale}_` : ''}\n`;
|
|
221
|
+
}
|
|
222
|
+
yield `\n`;
|
|
223
|
+
|
|
224
|
+
// ── Phase 2: EXECUTE ────────────────────────────────────────────
|
|
225
|
+
yield `### 2. Executing ${trimmed.length} subtask${trimmed.length === 1 ? '' : 's'}\n\n`;
|
|
226
|
+
const results = [];
|
|
227
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
228
|
+
const sub = trimmed[i];
|
|
229
|
+
const worker = workers[i % workers.length];
|
|
230
|
+
yield `**Subtask ${sub.id}** \`${worker.name}${worker.model ? ':' + worker.model : ''}\` — ${sub.task}\n\n`;
|
|
231
|
+
let res = '';
|
|
232
|
+
try {
|
|
233
|
+
for await (const chunk of worker.prov.sendMessage([{ role: 'user', content: sub.task }], {
|
|
234
|
+
apiKey: keyResolver(cfg, worker.name),
|
|
235
|
+
model: worker.model || undefined,
|
|
236
|
+
signal: callerOpts.signal,
|
|
237
|
+
})) {
|
|
238
|
+
const s = String(chunk);
|
|
239
|
+
res += s;
|
|
240
|
+
yield s;
|
|
241
|
+
}
|
|
242
|
+
results.push({ ...sub, worker: `${worker.name}${worker.model ? ':' + worker.model : ''}`, result: res, error: null });
|
|
243
|
+
} catch (e) {
|
|
244
|
+
const msg = e?.message || String(e);
|
|
245
|
+
yield `\n⚠ worker error: ${msg}\n`;
|
|
246
|
+
results.push({ ...sub, worker: `${worker.name}${worker.model ? ':' + worker.model : ''}`, result: '', error: msg });
|
|
247
|
+
}
|
|
248
|
+
yield `\n\n---\n\n`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Phase 3: SYNTHESIS ──────────────────────────────────────────
|
|
252
|
+
yield `### 3. Synthesis\n\n`;
|
|
253
|
+
const synthUser = [
|
|
254
|
+
`Original request:\n${userText}`,
|
|
255
|
+
`\nSubtask plan and worker outputs:`,
|
|
256
|
+
...results.map(r => `\n#### Subtask ${r.id} — ${r.task}\nWorker: ${r.worker}\n${r.error ? `Error: ${r.error}` : r.result.trim()}`),
|
|
257
|
+
`\nNow write the final answer for the user.`,
|
|
258
|
+
].join('\n');
|
|
259
|
+
try {
|
|
260
|
+
for await (const chunk of planner.prov.sendMessage([
|
|
261
|
+
{ role: 'system', content: SYNTHESIS_SYSTEM },
|
|
262
|
+
{ role: 'user', content: synthUser },
|
|
263
|
+
], {
|
|
264
|
+
apiKey: keyResolver(cfg, planner.name),
|
|
265
|
+
model: planner.model || undefined,
|
|
266
|
+
signal: callerOpts.signal,
|
|
267
|
+
})) yield String(chunk);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
yield `⚠ synthesis error: ${e?.message || String(e)}. Worker outputs above are the final material — please review them directly.\n`;
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
package/providers/registry.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { ollamaProvider } from './ollama.mjs';
|
|
|
13
13
|
import { geminiProvider } from './gemini.mjs';
|
|
14
14
|
import { claudeCliProvider } from './claude_cli.mjs';
|
|
15
15
|
import { makeOpenAICompatProvider, fetchOpenAICompatModels } from './openai_compat.mjs';
|
|
16
|
+
import { makeOrchestratorProvider } from './orchestrator.mjs';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* @typedef {{ role: 'user'|'assistant'|'system', content: string }} ChatMessage
|
|
@@ -51,6 +52,7 @@ export const mockProvider = {
|
|
|
51
52
|
|
|
52
53
|
export { anthropicProvider, openaiProvider, ollamaProvider, geminiProvider, claudeCliProvider };
|
|
53
54
|
export { makeOpenAICompatProvider, fetchOpenAICompatModels };
|
|
55
|
+
export { makeOrchestratorProvider };
|
|
54
56
|
|
|
55
57
|
// Built-in OpenAI-compatible vendors. Same wire format → one factory call
|
|
56
58
|
// each. The picker treats these like first-class providers so users don't
|
|
@@ -229,6 +231,13 @@ export const PROVIDERS = {
|
|
|
229
231
|
mock: mockProvider,
|
|
230
232
|
};
|
|
231
233
|
|
|
234
|
+
// Orchestrator — multi-agent dispatcher that composes other providers.
|
|
235
|
+
// Registered upfront with no cfg/keyResolver so a bare process can list
|
|
236
|
+
// it via `lazyclaw providers list`; `registerOrchestrator(...)` from
|
|
237
|
+
// cli.mjs::ensureRegistry wires in the live cfg + auth-key resolver so
|
|
238
|
+
// sendMessage can reach env vars / authProfiles / customProviders.
|
|
239
|
+
PROVIDERS.orchestrator = makeOrchestratorProvider();
|
|
240
|
+
|
|
232
241
|
// Wire each OpenAI-compat builtin into PROVIDERS as a callable provider.
|
|
233
242
|
// Insertion is between Tier 2 (anthropic) and Tier 4 (ollama) by reordering
|
|
234
243
|
// the keys after the loop runs — JS objects honour insertion order and
|
|
@@ -348,6 +357,21 @@ export const PROVIDER_INFO = {
|
|
|
348
357
|
},
|
|
349
358
|
};
|
|
350
359
|
|
|
360
|
+
// Orchestrator metadata. Composes other providers; the planner/workers
|
|
361
|
+
// each carry their own keys (or none for claude-cli / ollama / mock),
|
|
362
|
+
// so the orchestrator itself reports requiresApiKey: false. The setup
|
|
363
|
+
// picker treats it as a CLI/Local-family entry — no api-key prompt.
|
|
364
|
+
PROVIDER_INFO.orchestrator = {
|
|
365
|
+
name: 'orchestrator',
|
|
366
|
+
label: 'Orchestrator (multi-agent)',
|
|
367
|
+
requiresApiKey: false,
|
|
368
|
+
docs: 'Orchestrator — decomposes the user message into 2-5 parallel subtasks, dispatches each to a worker provider, then synthesizes the answers. Configure cfg.orchestrator = { planner: "provider:model", workers: ["provider:model", ...], maxSubtasks?: 5 }. Composes any registered provider — Claude / OpenAI / Gemini / NIM / Groq / local Ollama / custom OpenAI-compat endpoints.',
|
|
369
|
+
endpoint: '(composes other providers)',
|
|
370
|
+
defaultModel: 'orchestrator',
|
|
371
|
+
suggestedModels: ['orchestrator'],
|
|
372
|
+
composite: true,
|
|
373
|
+
};
|
|
374
|
+
|
|
351
375
|
// Mirror the OpenAI-compat builtins into PROVIDER_INFO so picker / docs /
|
|
352
376
|
// `lazyclaw providers info` see them with the same shape as the hand-written
|
|
353
377
|
// entries above.
|
|
@@ -370,6 +394,16 @@ for (const [name, def] of Object.entries(OPENAI_COMPAT_BUILTINS)) {
|
|
|
370
394
|
};
|
|
371
395
|
}
|
|
372
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Re-register PROVIDERS.orchestrator with a live config getter + auth-key
|
|
399
|
+
* resolver, so each phase's worker call can pick up env vars / authProfiles
|
|
400
|
+
* / customProviders. Called from cli.mjs::ensureRegistry on every entry
|
|
401
|
+
* — idempotent (overwrites the previous registration in place).
|
|
402
|
+
*/
|
|
403
|
+
export function registerOrchestrator({ cfgGetter, keyResolver } = {}) {
|
|
404
|
+
PROVIDERS.orchestrator = makeOrchestratorProvider({ cfgGetter, keyResolver });
|
|
405
|
+
}
|
|
406
|
+
|
|
373
407
|
/**
|
|
374
408
|
* Resolve an api-key for a built-in OpenAI-compatible provider from the
|
|
375
409
|
* environment, scanning {envKey} then any {altEnvKeys}. Returns '' when
|
|
@@ -422,6 +456,7 @@ export function parseProviderModel(s) {
|
|
|
422
456
|
// `makeOpenAICompatProvider` — overriding is well-defined.
|
|
423
457
|
const RESERVED_PROVIDER_NAMES = new Set([
|
|
424
458
|
'mock', 'claude-cli', 'anthropic', 'openai', 'gemini', 'ollama',
|
|
459
|
+
'orchestrator',
|
|
425
460
|
'__add_custom__', '__custom_model__', '__fetch_models__',
|
|
426
461
|
]);
|
|
427
462
|
|