lazyclaw 3.99.20 → 3.99.22
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 +43 -0
- package/cli.mjs +271 -1
- package/package.json +1 -1
- package/providers/orchestrator.mjs +281 -0
- package/providers/registry.mjs +35 -0
package/README.md
CHANGED
|
@@ -81,6 +81,49 @@ 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
|
+
|
|
114
|
+
You can skip the JSON entirely and configure via `lazyclaw onboard` / `lazyclaw setup` (the picker lands on the orchestrator and walks you through a planner + workers wizard) **or** via the dedicated CLI:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
lazyclaw orchestrator status
|
|
118
|
+
lazyclaw orchestrator set-planner claude-cli:claude-opus-4-7
|
|
119
|
+
lazyclaw orchestrator workers add openai:gpt-4o
|
|
120
|
+
lazyclaw orchestrator workers add gemini:gemini-2.5-pro
|
|
121
|
+
lazyclaw orchestrator workers set claude-cli:claude-sonnet-4-6,nim:meta/llama-3.1-405b-instruct # bulk replace
|
|
122
|
+
lazyclaw orchestrator set-max-subtasks 5
|
|
123
|
+
lazyclaw orchestrator clear # wipe cfg.orchestrator
|
|
124
|
+
lazyclaw config set provider orchestrator # route chats through it
|
|
125
|
+
```
|
|
126
|
+
|
|
84
127
|
## Launcher (no-arg `lazyclaw`)
|
|
85
128
|
|
|
86
129
|
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
|
|
|
@@ -902,6 +914,8 @@ const SUBCOMMANDS = [
|
|
|
902
914
|
'auth', 'pairing', 'nodes', 'message', 'workspace', 'browse', 'cron',
|
|
903
915
|
// v3.99.6 — multi-step setup wizard + lazyclaw-only dashboard
|
|
904
916
|
'setup', 'dashboard',
|
|
917
|
+
// v3.99.22 — multi-agent orchestrator config
|
|
918
|
+
'orchestrator',
|
|
905
919
|
];
|
|
906
920
|
|
|
907
921
|
const SUBCOMMAND_SUBS = {
|
|
@@ -917,6 +931,7 @@ const SUBCOMMAND_SUBS = {
|
|
|
917
931
|
message: ['list', 'add', 'remove', 'send'],
|
|
918
932
|
workspace: ['list', 'init', 'show', 'remove', 'path'],
|
|
919
933
|
cron: ['list', 'add', 'remove', 'show', 'sync', 'run'],
|
|
934
|
+
orchestrator: ['status', 'set-planner', 'workers', 'set-max-subtasks', 'clear'],
|
|
920
935
|
};
|
|
921
936
|
|
|
922
937
|
function bashCompletion() {
|
|
@@ -1173,6 +1188,7 @@ const HELP_DETAILS = {
|
|
|
1173
1188
|
cron: 'Usage: lazyclaw cron <list | add <name> "<cron-spec>" -- <cmd> ... | remove <name> | show <name> | sync | run <name>>\n Schedule recurring agent runs. macOS uses launchd (~/Library/LaunchAgents/com.lazyclaw.<name>.plist); Linux / WSL uses the user crontab.\n Cron spec is the standard 5-field form (minute hour dom month dow). Supports *, range a-b, list a,b,c, step */N.\n add: pass the command after `--`. Typical use:\n lazyclaw cron add daily-summary "0 9 * * 1-5" -- lazyclaw agent "Summarise today\'s TODOs"\n list / show: read from cfg.cron[name] (config is the source of truth).\n sync: re-installs every job in cfg.cron into the system scheduler — handy after a reinstall.\n run: one-shot in-process execution of the named job; the OS scheduler does the same thing on its trigger.\n Logs: ~/.lazyclaw/logs/cron-<name>.{out,err}.log (macOS launchd path).',
|
|
1174
1189
|
setup: 'Usage: lazyclaw setup [--skip-test]\n OpenClaw-style multi-step first-run wizard. Walks through:\n 1. Provider + model + api-key (delegates to onboard --pick)\n 2. Optional workspace init (AGENTS.md / SOUL.md / TOOLS.md)\n 3. Optional skill bundle install from GitHub\n 4. Optional outbound webhook (Slack / Discord)\n 5. Reachability test against the picked provider\n Each optional step takes Enter or "skip" to bypass. Re-runnable safely.\n Also fires automatically on first run when `lazyclaw` is invoked with no config.',
|
|
1175
1190
|
dashboard: 'Usage: lazyclaw dashboard [--port <N>] [--no-open]\n Launches the lazyclaw-only web UI on http://127.0.0.1:<port> (default 19600) and opens it in the default browser.\n Wraps `lazyclaw daemon` + a static HTML; no Python / lazyclaude dashboard required.\n Tabs: Chat · Sessions · Skills · Workspace · Providers · Status. Each tab calls existing daemon endpoints.\n --no-open keeps the browser closed (handy for SSH / headless / dev). The bound URL is always printed to stdout.',
|
|
1191
|
+
orchestrator: 'Usage: lazyclaw orchestrator <status | set-planner <provider[:model]> | workers add <spec> | workers remove <spec> | workers set <spec,spec,...> | workers clear | set-max-subtasks <N> | clear>\n Read/write cfg.orchestrator without editing config.json by hand.\n status — print {planner, workers, maxSubtasks} as JSON; lists registered providers for reference.\n set-planner — replace the planner spec ("provider" or "provider:model"). "orchestrator" itself is rejected (self-recursion).\n workers add — append a worker (idempotent — duplicates skipped).\n workers remove — drop a worker by exact match. Idempotent.\n workers set — replace the whole list (comma-separated specs).\n workers clear — empty the workers list.\n set-max-subtasks <N> — cap subtasks per request, clamped 1..10 (default 5).\n clear — delete the cfg.orchestrator block entirely.\n Pair with: `lazyclaw config set provider orchestrator` to route chats through it.',
|
|
1176
1192
|
};
|
|
1177
1193
|
|
|
1178
1194
|
function cmdHelp(name) {
|
|
@@ -1823,7 +1839,17 @@ async function _pickProviderInteractive() {
|
|
|
1823
1839
|
provider = picked;
|
|
1824
1840
|
}
|
|
1825
1841
|
|
|
1826
|
-
// ── Step 3 — model
|
|
1842
|
+
// ── Step 3 — model (or, for composite providers, a config wizard) ───
|
|
1843
|
+
// The orchestrator (and any future composite provider) has no model
|
|
1844
|
+
// of its own — it dispatches to other providers. Step 3 routes
|
|
1845
|
+
// through a custom wizard instead of the standard model picker.
|
|
1846
|
+
const providerMeta = (_registryMod.PROVIDER_INFO || {})[provider.id] || {};
|
|
1847
|
+
if (providerMeta.composite || provider.id === 'orchestrator') {
|
|
1848
|
+
const result = await _setupOrchestratorInteractive();
|
|
1849
|
+
if (result === 'CANCEL') return null;
|
|
1850
|
+
if (result === 'BACK') return _pickProviderInteractive();
|
|
1851
|
+
return { provider: provider.id, model: 'orchestrator' };
|
|
1852
|
+
}
|
|
1827
1853
|
const picked = await _pickModelInteractive(provider.id, {
|
|
1828
1854
|
titlePrefix: 'LazyClaw setup — Step 3 of 3:',
|
|
1829
1855
|
onBack: 'restart',
|
|
@@ -1833,6 +1859,125 @@ async function _pickProviderInteractive() {
|
|
|
1833
1859
|
return { provider: provider.id, model: picked };
|
|
1834
1860
|
}
|
|
1835
1861
|
|
|
1862
|
+
// Step-3 alternative for composite providers (currently only the
|
|
1863
|
+
// orchestrator). Builds `cfg.orchestrator = { planner, workers,
|
|
1864
|
+
// maxSubtasks }` interactively and persists it before returning.
|
|
1865
|
+
//
|
|
1866
|
+
// planner: single picker over registered non-composite providers.
|
|
1867
|
+
// workers: multi-select with a running list + add/remove/done loop.
|
|
1868
|
+
// maxSubtasks: typed integer, default 5.
|
|
1869
|
+
async function _setupOrchestratorInteractive() {
|
|
1870
|
+
const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
1871
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1872
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1873
|
+
const ok = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1874
|
+
const info = _registryMod.PROVIDER_INFO || {};
|
|
1875
|
+
const eligibleNames = Object.keys(_registryMod.PROVIDERS).filter((n) => n !== 'orchestrator' && n !== 'mock');
|
|
1876
|
+
if (eligibleNames.length === 0) {
|
|
1877
|
+
process.stdout.write('\n' + accent('orchestrator setup') + ': no eligible workers — register a real provider first.\n');
|
|
1878
|
+
await _quickPrompt(' press Enter to continue ');
|
|
1879
|
+
return 'CANCEL';
|
|
1880
|
+
}
|
|
1881
|
+
const cfg = readConfig();
|
|
1882
|
+
const existing = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
|
|
1883
|
+
|
|
1884
|
+
// ── Pick planner ─────────────────────────────────────────────────
|
|
1885
|
+
const plannerItems = eligibleNames.map((name) => {
|
|
1886
|
+
const m = info[name] || {};
|
|
1887
|
+
const defaultModel = m.defaultModel || '';
|
|
1888
|
+
return {
|
|
1889
|
+
id: `${name}${defaultModel ? ':' + defaultModel : ''}`,
|
|
1890
|
+
label: m.label && m.label !== name ? `${name} — ${m.label}` : name,
|
|
1891
|
+
desc: defaultModel ? `default model: ${defaultModel}` : '',
|
|
1892
|
+
};
|
|
1893
|
+
});
|
|
1894
|
+
const plannerPick = await _arrowMenu({
|
|
1895
|
+
title: 'LazyClaw setup — Step 3 of 3: orchestrator — pick the planner',
|
|
1896
|
+
subtitle: 'The planner decomposes the user request into subtasks and writes the final synthesis. Strong reasoning models work best here.',
|
|
1897
|
+
items: plannerItems,
|
|
1898
|
+
searchable: true,
|
|
1899
|
+
defaultIdx: Math.max(0, plannerItems.findIndex((p) => p.id === existing.planner)),
|
|
1900
|
+
});
|
|
1901
|
+
if (plannerPick === 'CANCEL') return 'CANCEL';
|
|
1902
|
+
if (plannerPick === 'BACK') return 'BACK';
|
|
1903
|
+
const planner = plannerPick.id;
|
|
1904
|
+
|
|
1905
|
+
// ── Pick workers (iterative add/remove) ──────────────────────────
|
|
1906
|
+
const workers = Array.isArray(existing.workers) ? existing.workers.slice() : [];
|
|
1907
|
+
while (true) {
|
|
1908
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1909
|
+
process.stdout.write(accent('Orchestrator workers') + '\n');
|
|
1910
|
+
process.stdout.write(dim('Subtasks are dispatched round-robin across this list.') + '\n\n');
|
|
1911
|
+
if (workers.length === 0) {
|
|
1912
|
+
process.stdout.write(' ' + dim('(none yet — add at least one)') + '\n\n');
|
|
1913
|
+
} else {
|
|
1914
|
+
workers.forEach((w, i) => {
|
|
1915
|
+
process.stdout.write(` ${i + 1}. ${ok(w)}\n`);
|
|
1916
|
+
});
|
|
1917
|
+
process.stdout.write('\n');
|
|
1918
|
+
}
|
|
1919
|
+
const items = [
|
|
1920
|
+
{ id: '__add__', label: '+ Add a worker', desc: 'pick from registered providers' },
|
|
1921
|
+
{ id: '__remove__', label: '- Remove a worker', desc: workers.length ? 'pick which entry to drop' : '(nothing to remove)' },
|
|
1922
|
+
{ id: '__done__', label: `Done${workers.length ? ` (${workers.length} worker${workers.length === 1 ? '' : 's'})` : ' — at least one worker required'}`, desc: workers.length ? 'save cfg.orchestrator and finish' : 'add one worker first' },
|
|
1923
|
+
];
|
|
1924
|
+
const action = await _arrowMenu({
|
|
1925
|
+
title: 'LazyClaw setup — orchestrator workers',
|
|
1926
|
+
subtitle: `Planner: ${planner}`,
|
|
1927
|
+
items,
|
|
1928
|
+
});
|
|
1929
|
+
if (action === 'CANCEL') return 'CANCEL';
|
|
1930
|
+
if (action === 'BACK') return 'BACK';
|
|
1931
|
+
if (action.id === '__add__') {
|
|
1932
|
+
const wPick = await _arrowMenu({
|
|
1933
|
+
title: 'Add worker',
|
|
1934
|
+
subtitle: 'Picked entries are appended to the workers list.',
|
|
1935
|
+
items: plannerItems.filter((p) => !workers.includes(p.id)),
|
|
1936
|
+
searchable: true,
|
|
1937
|
+
});
|
|
1938
|
+
if (wPick === 'CANCEL' || wPick === 'BACK') continue;
|
|
1939
|
+
workers.push(wPick.id);
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
if (action.id === '__remove__') {
|
|
1943
|
+
if (!workers.length) continue;
|
|
1944
|
+
const rPick = await _arrowMenu({
|
|
1945
|
+
title: 'Remove worker',
|
|
1946
|
+
subtitle: 'Highlighted entry is removed from the list.',
|
|
1947
|
+
items: workers.map((w) => ({ id: w, label: w })),
|
|
1948
|
+
});
|
|
1949
|
+
if (rPick === 'CANCEL' || rPick === 'BACK') continue;
|
|
1950
|
+
const idx = workers.indexOf(rPick.id);
|
|
1951
|
+
if (idx >= 0) workers.splice(idx, 1);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
if (action.id === '__done__') {
|
|
1955
|
+
if (workers.length === 0) continue;
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// ── maxSubtasks ──────────────────────────────────────────────────
|
|
1961
|
+
const defaultMax = Number.isFinite(existing.maxSubtasks) && existing.maxSubtasks > 0
|
|
1962
|
+
? Math.min(10, existing.maxSubtasks)
|
|
1963
|
+
: 5;
|
|
1964
|
+
const rawMax = (await _quickPrompt(` ${bold('maxSubtasks')} ${dim(`(2..10, blank → ${defaultMax}):`)} `)).trim();
|
|
1965
|
+
let maxSubtasks = defaultMax;
|
|
1966
|
+
if (rawMax) {
|
|
1967
|
+
const n = parseInt(rawMax, 10);
|
|
1968
|
+
if (Number.isFinite(n) && n >= 1) maxSubtasks = Math.min(10, Math.max(1, n));
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// ── Persist ──────────────────────────────────────────────────────
|
|
1972
|
+
cfg.orchestrator = { planner, workers, maxSubtasks };
|
|
1973
|
+
writeConfig(cfg);
|
|
1974
|
+
process.stdout.write('\n');
|
|
1975
|
+
process.stdout.write(` ${ok('✓ orchestrator saved')} ${dim('→')} ` +
|
|
1976
|
+
`planner ${ok(planner)} · ${workers.length} worker${workers.length === 1 ? '' : 's'} · maxSubtasks ${maxSubtasks}\n`);
|
|
1977
|
+
await _quickPrompt(' press Enter to continue ');
|
|
1978
|
+
return { ok: true };
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1836
1981
|
// Pause the chat REPL's readline + ghost-autocomplete while a sub-picker
|
|
1837
1982
|
// (provider / model arrow menu) takes over the terminal. The sub-picker
|
|
1838
1983
|
// installs its own `keypress` listener and toggles raw mode; the chat's
|
|
@@ -3710,6 +3855,126 @@ async function cmdProviders(sub, positional, flags = {}) {
|
|
|
3710
3855
|
}
|
|
3711
3856
|
}
|
|
3712
3857
|
|
|
3858
|
+
// `lazyclaw orchestrator` — read/write the cfg.orchestrator section
|
|
3859
|
+
// without editing config.json by hand. Mirrors the shape `lazyclaw
|
|
3860
|
+
// providers` / `lazyclaw rates` already use.
|
|
3861
|
+
//
|
|
3862
|
+
// Subcommands:
|
|
3863
|
+
// status Print current planner / workers / maxSubtasks as JSON.
|
|
3864
|
+
// set-planner <provider[:model]> Replace the planner spec.
|
|
3865
|
+
// workers add <provider[:model]> Append a worker (idempotent — duplicates skipped).
|
|
3866
|
+
// workers remove <provider[:model]> Drop a worker by exact match. Idempotent.
|
|
3867
|
+
// workers clear Empty the workers list.
|
|
3868
|
+
// workers set <provider[:model],...> Replace the whole list (comma-separated).
|
|
3869
|
+
// set-max-subtasks <N> Cap the number of subtasks (clamped 1..10).
|
|
3870
|
+
// clear Delete the entire cfg.orchestrator block.
|
|
3871
|
+
async function cmdOrchestrator(sub, positional, _flags = {}) {
|
|
3872
|
+
await ensureRegistry();
|
|
3873
|
+
const cfg = readConfig();
|
|
3874
|
+
const orch = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
|
|
3875
|
+
const known = Object.keys(_registryMod.PROVIDERS);
|
|
3876
|
+
const validateSpec = (spec) => {
|
|
3877
|
+
if (!spec) throw new Error('provider spec required (e.g. "claude-cli" or "openai:gpt-4o")');
|
|
3878
|
+
const colon = spec.indexOf(':');
|
|
3879
|
+
const provName = colon > 0 ? spec.slice(0, colon) : spec;
|
|
3880
|
+
if (provName === 'orchestrator') throw new Error('"orchestrator" cannot reference itself — pick a real provider');
|
|
3881
|
+
if (!known.includes(provName)) {
|
|
3882
|
+
throw new Error(`unknown provider "${provName}" — registered: ${known.join(', ')}`);
|
|
3883
|
+
}
|
|
3884
|
+
return spec;
|
|
3885
|
+
};
|
|
3886
|
+
const saveAndPrint = (next) => {
|
|
3887
|
+
if (next === null) delete cfg.orchestrator;
|
|
3888
|
+
else cfg.orchestrator = next;
|
|
3889
|
+
writeConfig(cfg);
|
|
3890
|
+
console.log(JSON.stringify(cfg.orchestrator || null, null, 2));
|
|
3891
|
+
};
|
|
3892
|
+
switch (sub) {
|
|
3893
|
+
case undefined:
|
|
3894
|
+
case 'status': {
|
|
3895
|
+
console.log(JSON.stringify({
|
|
3896
|
+
ok: true,
|
|
3897
|
+
configured: !!cfg.orchestrator,
|
|
3898
|
+
planner: orch.planner || null,
|
|
3899
|
+
workers: Array.isArray(orch.workers) ? orch.workers : [],
|
|
3900
|
+
maxSubtasks: Number.isFinite(orch.maxSubtasks) ? orch.maxSubtasks : null,
|
|
3901
|
+
knownProviders: known,
|
|
3902
|
+
}, null, 2));
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3905
|
+
case 'set-planner': {
|
|
3906
|
+
try {
|
|
3907
|
+
const spec = validateSpec(positional[0]);
|
|
3908
|
+
saveAndPrint({ ...orch, planner: spec });
|
|
3909
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3912
|
+
case 'workers': {
|
|
3913
|
+
const wsub = positional[0];
|
|
3914
|
+
const workers = Array.isArray(orch.workers) ? orch.workers.slice() : [];
|
|
3915
|
+
switch (wsub) {
|
|
3916
|
+
case 'add': {
|
|
3917
|
+
try {
|
|
3918
|
+
const spec = validateSpec(positional[1]);
|
|
3919
|
+
if (!workers.includes(spec)) workers.push(spec);
|
|
3920
|
+
saveAndPrint({ ...orch, workers });
|
|
3921
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
case 'remove': {
|
|
3925
|
+
const spec = positional[1];
|
|
3926
|
+
if (!spec) { console.error('orchestrator: workers remove <provider[:model]>'); process.exit(2); }
|
|
3927
|
+
const idx = workers.indexOf(spec);
|
|
3928
|
+
if (idx >= 0) workers.splice(idx, 1);
|
|
3929
|
+
saveAndPrint({ ...orch, workers });
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3932
|
+
case 'clear': {
|
|
3933
|
+
saveAndPrint({ ...orch, workers: [] });
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3936
|
+
case 'set': {
|
|
3937
|
+
const raw = positional[1] || '';
|
|
3938
|
+
const specs = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
3939
|
+
try {
|
|
3940
|
+
specs.forEach(validateSpec);
|
|
3941
|
+
saveAndPrint({ ...orch, workers: specs });
|
|
3942
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
3943
|
+
return;
|
|
3944
|
+
}
|
|
3945
|
+
default: {
|
|
3946
|
+
console.error('Usage: lazyclaw orchestrator workers <add <spec> | remove <spec> | clear | set <spec,spec,...>>');
|
|
3947
|
+
process.exit(2);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
case 'set-max-subtasks': {
|
|
3952
|
+
const n = parseInt(positional[0], 10);
|
|
3953
|
+
if (!Number.isFinite(n) || n < 1) { console.error('orchestrator: set-max-subtasks <N> (1..10)'); process.exit(2); }
|
|
3954
|
+
saveAndPrint({ ...orch, maxSubtasks: Math.min(10, Math.max(1, n)) });
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
case 'clear': {
|
|
3958
|
+
saveAndPrint(null);
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
default: {
|
|
3962
|
+
console.error(
|
|
3963
|
+
'Usage:\n' +
|
|
3964
|
+
' lazyclaw orchestrator status\n' +
|
|
3965
|
+
' lazyclaw orchestrator set-planner <provider[:model]>\n' +
|
|
3966
|
+
' lazyclaw orchestrator workers add <provider[:model]>\n' +
|
|
3967
|
+
' lazyclaw orchestrator workers remove <provider[:model]>\n' +
|
|
3968
|
+
' lazyclaw orchestrator workers set <provider[:model],...>\n' +
|
|
3969
|
+
' lazyclaw orchestrator workers clear\n' +
|
|
3970
|
+
' lazyclaw orchestrator set-max-subtasks <N>\n' +
|
|
3971
|
+
' lazyclaw orchestrator clear'
|
|
3972
|
+
);
|
|
3973
|
+
process.exit(2);
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3713
3978
|
async function cmdSessions(sub, positional, flags = {}) {
|
|
3714
3979
|
const sessionsMod = await import('./sessions.mjs');
|
|
3715
3980
|
const cfgDir = path.dirname(configPath());
|
|
@@ -4602,6 +4867,11 @@ async function main() {
|
|
|
4602
4867
|
await cmdProviders(sub, rest.positional.slice(1), rest.flags);
|
|
4603
4868
|
break;
|
|
4604
4869
|
}
|
|
4870
|
+
case 'orchestrator': {
|
|
4871
|
+
const sub = rest.positional[0];
|
|
4872
|
+
await cmdOrchestrator(sub, rest.positional.slice(1), rest.flags);
|
|
4873
|
+
break;
|
|
4874
|
+
}
|
|
4605
4875
|
case 'skills': {
|
|
4606
4876
|
const sub = rest.positional[0];
|
|
4607
4877
|
await cmdSkills(sub, rest.positional.slice(1), rest.flags);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "3.99.
|
|
3
|
+
"version": "3.99.22",
|
|
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,281 @@
|
|
|
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
|
+
// First-run hint when cfg.orchestrator is missing entirely. The
|
|
128
|
+
// fallback path below still works (planner = cfg.provider, single
|
|
129
|
+
// worker = same), but the user almost certainly meant to opt in
|
|
130
|
+
// explicitly — surface the shortest valid CLI to set it up.
|
|
131
|
+
if (!cfg.orchestrator) {
|
|
132
|
+
yield `> orchestrator: \`cfg.orchestrator\` is not set. Defaulting to a single-agent chain on \`${fallbackSpec}\`.\n` +
|
|
133
|
+
`> Configure properly: \`lazyclaw orchestrator set-planner ${fallbackSpec}\` then \`lazyclaw orchestrator workers add <provider:model>\` (one per agent).\n\n`;
|
|
134
|
+
}
|
|
135
|
+
const plannerSpec = String(o.planner || fallbackSpec);
|
|
136
|
+
const workerSpecs = Array.isArray(o.workers) && o.workers.length
|
|
137
|
+
? o.workers.map(String)
|
|
138
|
+
: [plannerSpec];
|
|
139
|
+
const maxSubtasks = Number.isFinite(o.maxSubtasks) && o.maxSubtasks > 0 ? Math.min(10, o.maxSubtasks) : 5;
|
|
140
|
+
|
|
141
|
+
const planner = _lookupProvider(plannerSpec);
|
|
142
|
+
if (!planner) {
|
|
143
|
+
yield `⚠ orchestrator: planner provider "${plannerSpec}" is not registered. ` +
|
|
144
|
+
`Set cfg.orchestrator.planner to a valid "provider:model" (e.g. "claude-cli:claude-opus-4-7").\n`;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Self-recursion guard: a misconfigured cfg.orchestrator.planner =
|
|
148
|
+
// "orchestrator" would otherwise spin forever, with each call
|
|
149
|
+
// dispatching back to itself.
|
|
150
|
+
if (planner.name === 'orchestrator') {
|
|
151
|
+
yield `⚠ orchestrator: planner cannot be "orchestrator" — set cfg.orchestrator.planner to a real provider (e.g. "claude-cli:claude-opus-4-7").\n`;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const workers = workerSpecs.map(_lookupProvider).filter(Boolean).filter(w => w.name !== 'orchestrator');
|
|
155
|
+
if (workers.length === 0) {
|
|
156
|
+
yield `⚠ orchestrator: no usable workers (cfg.orchestrator.workers is empty, all unknown, or only references "orchestrator" itself).\n`;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const userText = (() => {
|
|
161
|
+
// Most recent user message becomes the orchestration target. We
|
|
162
|
+
// pass earlier turns as context to the planner only — workers
|
|
163
|
+
// see a self-contained subtask string, not chat history.
|
|
164
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
165
|
+
if (messages[i].role === 'user') return String(messages[i].content || '');
|
|
166
|
+
}
|
|
167
|
+
return '';
|
|
168
|
+
})();
|
|
169
|
+
|
|
170
|
+
// ── Phase 1: PLAN ───────────────────────────────────────────────
|
|
171
|
+
yield `## 🦞 Orchestrator\n\n`;
|
|
172
|
+
yield `Planner: \`${planner.name}${planner.model ? ':' + planner.model : ''}\` · Workers: ${workers.map(w => `\`${w.name}${w.model ? ':' + w.model : ''}\``).join(', ')}\n\n`;
|
|
173
|
+
yield `### 1. Planning\n\n`;
|
|
174
|
+
|
|
175
|
+
const plannerMessages = [
|
|
176
|
+
{ role: 'system', content: PLANNER_SYSTEM },
|
|
177
|
+
...messages.filter(m => m.role === 'user' || m.role === 'assistant'),
|
|
178
|
+
];
|
|
179
|
+
let planRaw = '';
|
|
180
|
+
try {
|
|
181
|
+
for await (const chunk of planner.prov.sendMessage(plannerMessages, {
|
|
182
|
+
apiKey: keyResolver(cfg, planner.name),
|
|
183
|
+
model: planner.model || undefined,
|
|
184
|
+
signal: callerOpts.signal,
|
|
185
|
+
maxTokens: 1024,
|
|
186
|
+
})) {
|
|
187
|
+
planRaw += String(chunk);
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
yield `⚠ planner error: ${e?.message || String(e)}\n\n`;
|
|
191
|
+
// Fallback: hand the user message to the first worker directly.
|
|
192
|
+
const w = workers[0];
|
|
193
|
+
yield `Falling back to direct call on \`${w.name}${w.model ? ':' + w.model : ''}\`:\n\n`;
|
|
194
|
+
for await (const chunk of w.prov.sendMessage(messages, {
|
|
195
|
+
apiKey: keyResolver(cfg, w.name),
|
|
196
|
+
model: w.model || undefined,
|
|
197
|
+
signal: callerOpts.signal,
|
|
198
|
+
})) yield String(chunk);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const plan = _bestPlanArray(planRaw);
|
|
203
|
+
if (!plan || plan.length === 0) {
|
|
204
|
+
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`;
|
|
205
|
+
for await (const chunk of planner.prov.sendMessage(messages, {
|
|
206
|
+
apiKey: keyResolver(cfg, planner.name),
|
|
207
|
+
model: planner.model || undefined,
|
|
208
|
+
signal: callerOpts.signal,
|
|
209
|
+
})) yield String(chunk);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const trimmed = plan.slice(0, maxSubtasks).map((p, i) => ({
|
|
213
|
+
id: Number.isFinite(p?.id) ? p.id : i + 1,
|
|
214
|
+
task: String(p?.task || '').trim(),
|
|
215
|
+
rationale: String(p?.rationale || '').trim(),
|
|
216
|
+
})).filter(p => p.task);
|
|
217
|
+
if (trimmed.length === 0) {
|
|
218
|
+
yield `⚠ plan parsed but contained no usable subtasks. Falling back.\n\n`;
|
|
219
|
+
for await (const chunk of planner.prov.sendMessage(messages, {
|
|
220
|
+
apiKey: keyResolver(cfg, planner.name),
|
|
221
|
+
model: planner.model || undefined,
|
|
222
|
+
signal: callerOpts.signal,
|
|
223
|
+
})) yield String(chunk);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const p of trimmed) {
|
|
228
|
+
yield `${p.id}. **${p.task}**${p.rationale ? ` _— ${p.rationale}_` : ''}\n`;
|
|
229
|
+
}
|
|
230
|
+
yield `\n`;
|
|
231
|
+
|
|
232
|
+
// ── Phase 2: EXECUTE ────────────────────────────────────────────
|
|
233
|
+
yield `### 2. Executing ${trimmed.length} subtask${trimmed.length === 1 ? '' : 's'}\n\n`;
|
|
234
|
+
const results = [];
|
|
235
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
236
|
+
const sub = trimmed[i];
|
|
237
|
+
const worker = workers[i % workers.length];
|
|
238
|
+
yield `**Subtask ${sub.id}** \`${worker.name}${worker.model ? ':' + worker.model : ''}\` — ${sub.task}\n\n`;
|
|
239
|
+
let res = '';
|
|
240
|
+
try {
|
|
241
|
+
for await (const chunk of worker.prov.sendMessage([{ role: 'user', content: sub.task }], {
|
|
242
|
+
apiKey: keyResolver(cfg, worker.name),
|
|
243
|
+
model: worker.model || undefined,
|
|
244
|
+
signal: callerOpts.signal,
|
|
245
|
+
})) {
|
|
246
|
+
const s = String(chunk);
|
|
247
|
+
res += s;
|
|
248
|
+
yield s;
|
|
249
|
+
}
|
|
250
|
+
results.push({ ...sub, worker: `${worker.name}${worker.model ? ':' + worker.model : ''}`, result: res, error: null });
|
|
251
|
+
} catch (e) {
|
|
252
|
+
const msg = e?.message || String(e);
|
|
253
|
+
yield `\n⚠ worker error: ${msg}\n`;
|
|
254
|
+
results.push({ ...sub, worker: `${worker.name}${worker.model ? ':' + worker.model : ''}`, result: '', error: msg });
|
|
255
|
+
}
|
|
256
|
+
yield `\n\n---\n\n`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Phase 3: SYNTHESIS ──────────────────────────────────────────
|
|
260
|
+
yield `### 3. Synthesis\n\n`;
|
|
261
|
+
const synthUser = [
|
|
262
|
+
`Original request:\n${userText}`,
|
|
263
|
+
`\nSubtask plan and worker outputs:`,
|
|
264
|
+
...results.map(r => `\n#### Subtask ${r.id} — ${r.task}\nWorker: ${r.worker}\n${r.error ? `Error: ${r.error}` : r.result.trim()}`),
|
|
265
|
+
`\nNow write the final answer for the user.`,
|
|
266
|
+
].join('\n');
|
|
267
|
+
try {
|
|
268
|
+
for await (const chunk of planner.prov.sendMessage([
|
|
269
|
+
{ role: 'system', content: SYNTHESIS_SYSTEM },
|
|
270
|
+
{ role: 'user', content: synthUser },
|
|
271
|
+
], {
|
|
272
|
+
apiKey: keyResolver(cfg, planner.name),
|
|
273
|
+
model: planner.model || undefined,
|
|
274
|
+
signal: callerOpts.signal,
|
|
275
|
+
})) yield String(chunk);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
yield `⚠ synthesis error: ${e?.message || String(e)}. Worker outputs above are the final material — please review them directly.\n`;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
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
|
|